diff --git a/customModules/PollVar.ts b/customModules/PollVar.ts index 620358e..8ce95b9 100644 --- a/customModules/PollVar.ts +++ b/customModules/PollVar.ts @@ -69,7 +69,6 @@ export const pollVariableBash = ( }); }; - // Set up the interval initially with the provided polling interval Utils.merge([pollingInterval, ...trackers], (pollIntrvl: number) => { intervalFn(pollIntrvl); }); diff --git a/customModules/cpu/computeCPU.ts b/customModules/cpu/computeCPU.ts index ef3542d..83047d3 100644 --- a/customModules/cpu/computeCPU.ts +++ b/customModules/cpu/computeCPU.ts @@ -1,8 +1,6 @@ // @ts-expect-error import GTop from 'gi://GTop'; -const defaultCpuData: number = 0; - let previousCpuData = new GTop.glibtop_cpu(); GTop.glibtop_get_cpu(previousCpuData); diff --git a/customModules/cpu/index.ts b/customModules/cpu/index.ts index d10b15e..efe167e 100644 --- a/customModules/cpu/index.ts +++ b/customModules/cpu/index.ts @@ -1,8 +1,5 @@ import options from "options"; -// @ts-expect-error -import GTop from 'gi://GTop'; - // Module initializer import { module } from "../module" diff --git a/customModules/kblayout/getLayout.ts b/customModules/kblayout/getLayout.ts index 97a27ec..c622eb8 100644 --- a/customModules/kblayout/getLayout.ts +++ b/customModules/kblayout/getLayout.ts @@ -1,4 +1,4 @@ -import { HyprctlDeviceLayout, HyprctlKeyboard, KbLabelType } from "lib/types/customModules/kbLayout"; +import { HyprctlDeviceLayout, HyprctlKeyboard, KbLabelType, LayoutKeys, LayoutValues } from "lib/types/customModules/kbLayout"; import { layoutMap } from "./layouts"; export const getKeyboardLayout = (obj: string, format: KbLabelType) => { @@ -15,7 +15,8 @@ export const getKeyboardLayout = (obj: string, format: KbLabelType) => { mainKb = keyboards[keyboards.length - 1]; } - let layout = mainKb['active_keymap']; + let layout: LayoutKeys = mainKb['active_keymap'] as LayoutKeys; + const foundLayout: LayoutValues = layoutMap[layout]; - return format === "code" ? layoutMap[layout] || layout : layout; + return format === "code" ? foundLayout || layout : layout; } diff --git a/customModules/kblayout/layouts.ts b/customModules/kblayout/layouts.ts index ff4b0e1..1219e89 100644 --- a/customModules/kblayout/layouts.ts +++ b/customModules/kblayout/layouts.ts @@ -581,4 +581,4 @@ export const layoutMap = { "Wolof": "SN", "Yakut": "RU (Sah)", "Yoruba": "NG (Yoruba)" -}; +} as const; diff --git a/customModules/module.ts b/customModules/module.ts index 6e38ffa..681c18a 100644 --- a/customModules/module.ts +++ b/customModules/module.ts @@ -1,4 +1,5 @@ import { Module } from "lib/types/bar"; +import { BarButtonStyles } from "lib/types/options"; import options from "options"; import Gtk from "types/@girs/gtk-3.0/gtk-3.0"; import { Binding } from "types/service"; @@ -40,7 +41,7 @@ export const module = ({ return { component: Widget.Box({ - className: Utils.merge([style.bind("value"), showLabelBinding], (style: string, shwLabel: boolean) => { + className: Utils.merge([style.bind("value"), showLabelBinding], (style: BarButtonStyles, shwLabel: boolean) => { const shouldShowLabel = shwLabel || showLabel; const styleMap = { default: "style1", diff --git a/customModules/utils.ts b/customModules/utils.ts index 63bf4e1..3ede165 100644 --- a/customModules/utils.ts +++ b/customModules/utils.ts @@ -1,5 +1,6 @@ import { ResourceLabelType } from 'lib/types/bar'; import { GenericResourceData } from 'lib/types/customModules/generic'; +import { InputHandlerEvents } from 'lib/types/customModules/utils'; import { Binding } from 'lib/utils'; import { openMenu } from 'modules/bar/utils'; import options from 'options'; @@ -75,7 +76,7 @@ export const inputHandler = ( onMiddleClick, onScrollUp, onScrollDown, - } + }: InputHandlerEvents ) => { const sanitizeInput = (input: VariableType): string => { if (input === undefined) { diff --git a/globals.d.ts b/globals.d.ts index 9c92638..bb696c0 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -1,9 +1,12 @@ // globals.d.ts -import { Variable as VariableType } from "types/variable"; +import { Options, Variable as VariableType } from "types/variable"; declare global { var globalMousePos: VariableType; + var useTheme: Function; + var globalWeatherVar: VariableType; + var options: Options } export { }; diff --git a/globals/network.ts b/globals/network.ts new file mode 100644 index 0000000..fe0d1c5 --- /dev/null +++ b/globals/network.ts @@ -0,0 +1,16 @@ +export const WIFI_STATUS_MAP = { + unknown: "Status Unknown", + unmanaged: "Unmanaged", + unavailable: "Unavailable", + disconnected: "Disconnected", + prepare: "Preparing Connecting", + config: "Connecting", + need_auth: "Needs Authentication", + ip_config: "Requesting IP", + ip_check: "Checking Access", + secondaries: "Waiting on Secondaries", + activated: "Connected", + deactivating: "Disconnecting", + failed: "Connection Failed", +} as const; + diff --git a/globals/notification.ts b/globals/notification.ts new file mode 100644 index 0000000..0c918e6 --- /dev/null +++ b/globals/notification.ts @@ -0,0 +1,24 @@ +import icons from "modules/icons/index"; + +export const getNotificationIcon = (app_name: string, app_icon: string, app_entry: string) => { + let icon: string = icons.fallback.notification; + + if (Utils.lookUpIcon(app_name) || Utils.lookUpIcon(app_name.toLowerCase() || "")) { + icon = Utils.lookUpIcon(app_name) + ? app_name + : Utils.lookUpIcon(app_name.toLowerCase()) + ? app_name.toLowerCase() + : ""; + } + + if (Utils.lookUpIcon(app_icon) && icon === "") { + icon = app_icon; + } + + if (Utils.lookUpIcon(app_entry || "") && icon === "") { + icon = app_entry || ""; + } + + return icon; +}; + diff --git a/globals/useTheme.ts b/globals/useTheme.ts index eecf3ac..eb6bfa0 100644 --- a/globals/useTheme.ts +++ b/globals/useTheme.ts @@ -3,6 +3,8 @@ import { bash, Notify } from "lib/utils"; import icons from "lib/icons" import { filterConfigForThemeOnly, loadJsonFile, saveConfigToFile } from "widget/settings/shared/FileChooser"; +export const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; + globalThis.useTheme = (filePath: string): void => { let importedConfig = loadJsonFile(filePath); diff --git a/globals/variables.ts b/globals/variables.ts new file mode 100644 index 0000000..b207a14 --- /dev/null +++ b/globals/variables.ts @@ -0,0 +1,13 @@ +import { Opt } from "lib/option"; +import { HexColor, RecursiveOptionsObject } from "lib/types/options"; + +export const isOpt = (value: unknown): value is Opt => + typeof value === 'object' && value !== null && 'value' in value && value instanceof Opt; + +export const isRecursiveOptionsObject = (value: unknown): value is RecursiveOptionsObject => { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +export const isHexColor = (value: string): value is HexColor => { + return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value); +} diff --git a/globals/weather.ts b/globals/weather.ts index 7a0b54c..6e9e43e 100644 --- a/globals/weather.ts +++ b/globals/weather.ts @@ -1,9 +1,7 @@ import options from "options"; -import { UnitType, Weather } from "lib/types/weather.js"; +import { UnitType, Weather, WeatherIconTitle, WeatherIcon } from "lib/types/weather.js"; import { DEFAULT_WEATHER } from "lib/types/defaults/weather.js"; -import GLib from "gi://GLib?version=2.0" - -import icons from "../modules/icons/index.js"; +import GLib from "gi://GLib?version=2.0"; import { weatherIcons } from "modules/icons/weather.js"; const { key, interval, location } = options.menus.clock.weather; @@ -26,16 +24,16 @@ const weatherIntervalFn = (weatherInterval: number, loc: string, weatherKey: str .then((res) => { try { if (typeof res !== "string") { - return globalWeatherVar.value = DEFAULT_WEATHER; + return (globalWeatherVar.value = DEFAULT_WEATHER); } const parsedWeather = JSON.parse(res); if (Object.keys(parsedWeather).includes("error")) { - return globalWeatherVar.value = DEFAULT_WEATHER; + return (globalWeatherVar.value = DEFAULT_WEATHER); } - return globalWeatherVar.value = parsedWeather; + return (globalWeatherVar.value = parsedWeather); } catch (error) { globalWeatherVar.value = DEFAULT_WEATHER; console.warn(`Failed to parse weather data: ${error}`); @@ -45,12 +43,12 @@ const weatherIntervalFn = (weatherInterval: number, loc: string, weatherKey: str console.error(`Failed to fetch weather: ${err}`); globalWeatherVar.value = DEFAULT_WEATHER; }); - }) + }); }; Utils.merge([key.bind("value"), interval.bind("value"), location.bind("value")], (weatherKey, weatherInterval, loc) => { if (!weatherKey) { - return globalWeatherVar.value = DEFAULT_WEATHER; + return (globalWeatherVar.value = DEFAULT_WEATHER); } weatherIntervalFn(weatherInterval, loc, weatherKey); }); @@ -70,23 +68,28 @@ export const getWeatherIcon = (fahren: number) => { 50: "", 25: "", 0: "", - }; + } as const; const colors = { 100: "weather-color red", 75: "weather-color orange", 50: "weather-color lavender", 25: "weather-color blue", 0: "weather-color sky", - }; + } as const; - const threshold = + type IconKeys = keyof typeof icons; + + const threshold: IconKeys = fahren < 0 ? 0 - : [100, 75, 50, 25, 0].find((threshold) => threshold <= fahren); + : ([100, 75, 50, 25, 0] as IconKeys[]).find((threshold) => threshold <= fahren) || 0; + + const icon = icons[threshold || 50]; + const color = colors[threshold || 50]; return { - icon: icons[threshold || 50], - color: colors[threshold || 50], + icon, + color, }; }; @@ -95,11 +98,15 @@ export const getWindConditions = (wthr: Weather, unt: UnitType) => { return `${Math.floor(wthr.current.wind_mph)} mph`; } return `${Math.floor(wthr.current.wind_kph)} kph`; -} +}; export const getRainChance = (wthr: Weather) => `${wthr.forecast.forecastday[0].day.daily_chance_of_rain}%`; -export const getWeatherStatusTextIcon = (wthr: Weather) => { +export const isValidWeatherIconTitle = (title: string): title is WeatherIconTitle => { + return title in weatherIcons; +}; + +export const getWeatherStatusTextIcon = (wthr: Weather): WeatherIcon => { let iconQuery = wthr.current.condition.text .trim() .toLowerCase() @@ -108,7 +115,14 @@ export const getWeatherStatusTextIcon = (wthr: Weather) => { if (!wthr.current.is_day && iconQuery === "partly_cloudy") { iconQuery = "partly_cloudy_night"; } - return weatherIcons[iconQuery]; + + if (isValidWeatherIconTitle(iconQuery)) { + return weatherIcons[iconQuery]; + } else { + console.warn(`Unknown weather icon title: ${iconQuery}`); + return weatherIcons["warning"]; + } }; globalThis["globalWeatherVar"] = globalWeatherVar; + diff --git a/globals/window.ts b/globals/window.ts new file mode 100644 index 0000000..c675449 --- /dev/null +++ b/globals/window.ts @@ -0,0 +1,10 @@ +export const WINDOW_LAYOUTS: string[] = [ + 'center', + 'top', + 'top-right', + 'top-center', + 'top-left', + 'bottom-left', + 'bottom-center', + 'bottom-right' +]; diff --git a/lib/icons.ts b/lib/icons.ts index f6da697..6eb48e2 100644 --- a/lib/icons.ts +++ b/lib/icons.ts @@ -10,7 +10,7 @@ export const substitutes = { "preferences-system": "emblem-system-symbolic", "com.github.Aylur.ags-symbolic": "controls-symbolic", "com.github.Aylur.ags": "controls-symbolic", -} +} as const; export default { missing: "image-missing-symbolic", diff --git a/lib/option.ts b/lib/option.ts index 1428aff..609f1e4 100644 --- a/lib/option.ts +++ b/lib/option.ts @@ -1,3 +1,4 @@ +import { isHexColor } from "globals/variables" import { Variable } from "resource:///com/github/Aylur/ags/variable.js" type OptProps = { @@ -49,7 +50,7 @@ export class Opt extends Variable { if (this.persistent) return; - const isColor = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(`${this.value}`); + const isColor = isHexColor(this.value as string); if ((JSON.stringify(this.value) !== JSON.stringify(this.initial)) && isColor) { this.value = this.initial return this.id @@ -60,35 +61,37 @@ export class Opt extends Variable { export const opt = (initial: T, opts?: OptProps) => new Opt(initial, opts) -function getOptions(object: object, path = ""): Opt[] { +function getOptions(object: Record, path = ""): Opt[] { return Object.keys(object).flatMap(key => { - const obj: Opt = object[key] - const id = path ? path + "." + key : key + const obj = object[key]; + const id = path ? path + "." + key : key; if (obj instanceof Variable) { - obj.id = id - return obj + const optValue = obj as Opt; + optValue.id = id; + return optValue; } - if (typeof obj === "object") - return getOptions(obj, id) + if (typeof obj === "object" && obj !== null) { + return getOptions(obj as Record, id); // Recursively process nested objects + } - return [] - }) + return []; + }); } export function mkOptions(cacheFile: string, object: T, confFile: string = "config.json") { - for (const opt of getOptions(object)) + for (const opt of getOptions(object as Record)) opt.init(cacheFile) Utils.ensureDirectory(cacheFile.split("/").slice(0, -1).join("/")) const configFile = `${TMP}/${confFile}` - const values = getOptions(object).reduce((obj, { id, value }) => ({ [id]: value, ...obj }), {}) + const values = getOptions(object as Record).reduce((obj, { id, value }) => ({ [id]: value, ...obj }), {}) Utils.writeFileSync(JSON.stringify(values, null, 2), configFile) Utils.monitorFile(configFile, () => { const cache = JSON.parse(Utils.readFile(configFile) || "{}") - for (const opt of getOptions(object)) { + for (const opt of getOptions(object as Record)) { if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value)) opt.value = cache[opt.id] } @@ -99,7 +102,7 @@ export function mkOptions(cacheFile: string, object: T, confFi } async function reset( - [opt, ...list] = getOptions(object), + [opt, ...list] = getOptions(object as Record), id = opt?.reset(), ): Promise> { if (!opt) @@ -111,7 +114,7 @@ export function mkOptions(cacheFile: string, object: T, confFi } async function resetTheme( - [opt, ...list] = getOptions(object), + [opt, ...list] = getOptions(object as Record), id = opt?.doResetColor(), ): Promise> { if (!opt) @@ -124,7 +127,7 @@ export function mkOptions(cacheFile: string, object: T, confFi return Object.assign(object, { configFile, - array: () => getOptions(object), + array: () => getOptions(object as Record), async reset() { return (await reset()).join("\n") }, @@ -132,7 +135,7 @@ export function mkOptions(cacheFile: string, object: T, confFi return (await resetTheme()).join("\n") }, handler(deps: string[], callback: () => void) { - for (const opt of getOptions(object)) { + for (const opt of getOptions(object as Record)) { if (deps.some(i => opt.id.startsWith(i))) opt.connect("changed", callback) } diff --git a/lib/types/customModules/kbLayout.d.ts b/lib/types/customModules/kbLayout.d.ts index aee1464..94f41d3 100644 --- a/lib/types/customModules/kbLayout.d.ts +++ b/lib/types/customModules/kbLayout.d.ts @@ -1,3 +1,5 @@ +import { layoutMap } from "customModules/kblayout/layouts"; + export type KbLabelType = "layout" | "code"; export type KbIcon = "" | "󰌌" | "" | "󰬴" | "󰗊"; @@ -26,3 +28,6 @@ export type HyprctlDeviceLayout = { touch: any[]; switches: any[]; }; + +export type LayoutKeys = keyof typeof layoutMap; +export type LayoutValues = typeof layoutMap[LayoutKeys]; diff --git a/lib/types/customModules/utils.d.ts b/lib/types/customModules/utils.d.ts index e69de29..67583f6 100644 --- a/lib/types/customModules/utils.d.ts +++ b/lib/types/customModules/utils.d.ts @@ -0,0 +1,9 @@ +import { Binding } from "lib/utils"; + +export type InputHandlerEvents = { + onPrimaryClick?: Binding, + onSecondaryClick?: Binding, + onMiddleClick?: Binding, + onScrollUp?: Binding, + onScrollDown?: Binding, +} diff --git a/lib/types/dropdownmenu.d.ts b/lib/types/dropdownmenu.d.ts new file mode 100644 index 0000000..1aff3eb --- /dev/null +++ b/lib/types/dropdownmenu.d.ts @@ -0,0 +1,10 @@ +import { WindowProps } from "types/widgets/window"; + +export type DropdownMenuProps = { + name: string; + child: any; + layout?: string; + transition?: any; + exclusivity?: Exclusivity; + fixed?: boolean; +} & WindowProps; diff --git a/lib/types/filechooser.d.ts b/lib/types/filechooser.d.ts new file mode 100644 index 0000000..95e9ade --- /dev/null +++ b/lib/types/filechooser.d.ts @@ -0,0 +1,3 @@ +export type Config = { + [key: string]: string | number | boolean | object; +} diff --git a/lib/types/mpris.d.ts b/lib/types/mpris.d.ts new file mode 100644 index 0000000..18dcc61 --- /dev/null +++ b/lib/types/mpris.d.ts @@ -0,0 +1,3 @@ +export type LoopStatus = 'none' | 'track' | 'playlist'; +export type PlaybackStatus = 'playing' | 'paused' | 'stopped'; + diff --git a/lib/types/network.d.ts b/lib/types/network.d.ts index 63c8804..9877d0a 100644 --- a/lib/types/network.d.ts +++ b/lib/types/network.d.ts @@ -1,3 +1,5 @@ +import { WIFI_STATUS_MAP } from "globals/network"; + export type AccessPoint = { bssid: string | null; address: string | null; @@ -8,3 +10,5 @@ export type AccessPoint = { frequency: number; iconName: string | undefined; } + +export type WifiStatus = keyof typeof WIFI_STATUS_MAP; diff --git a/lib/types/notification.d.ts b/lib/types/notification.d.ts index 60f8dea..7ea4b8a 100644 --- a/lib/types/notification.d.ts +++ b/lib/types/notification.d.ts @@ -1,3 +1,5 @@ +import icons from "modules/icons/index"; + export interface NotificationArgs { appName?: string; body?: string; @@ -9,3 +11,5 @@ export interface NotificationArgs { timeout?: number; transient?: boolean; } + +export type NotificationIcon = keyof typeof icons.notifications; diff --git a/lib/types/options.d.ts b/lib/types/options.d.ts index 9c6330e..5474258 100644 --- a/lib/types/options.d.ts +++ b/lib/types/options.d.ts @@ -1,6 +1,10 @@ import { Opt } from "lib/option"; import { Variable } from "types/variable"; +export type RecursiveOptionsObject = { + [key: string]: RecursiveOptionsObject | Opt | Opt; +}; + export type Unit = "imperial" | "metric"; export type PowerOptions = "sleep" | "reboot" | "logout" | "shutdown"; export type NotificationAnchor = "top" | "top right" | "top left" | "bottom" | "bottom right" | "bottom left" | "left" | "right"; @@ -117,3 +121,4 @@ type MatugenVariation = | "vivid_3" type MatugenTheme = "light" | "dark"; + diff --git a/lib/types/popupwindow.d.ts b/lib/types/popupwindow.d.ts new file mode 100644 index 0000000..cc769a5 --- /dev/null +++ b/lib/types/popupwindow.d.ts @@ -0,0 +1,27 @@ +import { Widget } from "types/widgets/widget"; +import { WindowProps } from "types/widgets/window"; +import { Transition } from "./widget"; + +export type PopupWindowProps = { + name: string; + child: any; + layout?: Layouts; + transition?: any; + exclusivity?: Exclusivity; +} & WindowProps; + +export type LayoutFunction = ( + name: string, + child: Widget, + transition: Transition +) => { + center: () => Widget; + top: () => Widget; + "top-right": () => Widget; + "top-center": () => Widget; + "top-left": () => Widget; + "bottom-left": () => Widget; + "bottom-center": () => Widget; + "bottom-right": () => Widget; +}; +export type Layouts = 'center' | 'top' | 'top-right' | 'top-center' | 'top-left' | 'bottom-left' | 'bottom-center' | 'bottom-right'; diff --git a/lib/types/powerprofiles.d.ts b/lib/types/powerprofiles.d.ts new file mode 100644 index 0000000..976c43e --- /dev/null +++ b/lib/types/powerprofiles.d.ts @@ -0,0 +1,8 @@ +import icons from "modules/icons/index"; +import PowerProfiles from "types/service/powerprofiles.js" + +export type PowerProfiles = InstanceType; +export type PowerProfile = "power-saver" | "balanced" | "performance"; +export type PowerProfileObject = { + [key: string]: string; +} diff --git a/lib/types/utils.d.ts b/lib/types/utils.d.ts new file mode 100644 index 0000000..6ff3a55 --- /dev/null +++ b/lib/types/utils.d.ts @@ -0,0 +1,3 @@ +import { substitutes } from "lib/icons"; + +type SubstituteKeys = keyof typeof substitutes; diff --git a/lib/types/weather.d.ts b/lib/types/weather.d.ts index 54753ab..5a8f03c 100644 --- a/lib/types/weather.d.ts +++ b/lib/types/weather.d.ts @@ -1,3 +1,5 @@ +import { weatherIcons } from "modules/icons/weather"; + export type UnitType = "imperial" | "metric"; export type Weather = { @@ -107,3 +109,10 @@ export type Location = { localtime_epoch: number; localtime: string; } + +export type TemperatureIconColorMap = { + [key: number]: string; +} + +export type WeatherIconTitle = keyof typeof weatherIcons; +export type WeatherIcon = typeof weatherIcons[WeatherIconTitle]; diff --git a/lib/types/widget.d.ts b/lib/types/widget.d.ts index 7fcda61..5093a3e 100644 --- a/lib/types/widget.d.ts +++ b/lib/types/widget.d.ts @@ -1,3 +1,6 @@ export type Exclusivity = 'normal' | 'ignore' | 'exclusive'; export type Anchor = "left" | "right" | "top" | "down"; export type Transition = "none" | "crossfade" | "slide_right" | "slide_left" | "slide_up" | "slide_down"; + +// Window +export type Layouts = 'center' | 'top' | 'top-right' | 'top-center' | 'top-left' | 'bottom-left' | 'bottom-center' | 'bottom-right'; diff --git a/lib/utils.ts b/lib/utils.ts index 10446b9..71f4af6 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -8,20 +8,29 @@ import Gdk from "gi://Gdk" import GLib from "gi://GLib?version=2.0" import GdkPixbuf from "gi://GdkPixbuf"; import { NotificationArgs } from "types/utils/notify" +import { SubstituteKeys } from "./types/utils"; export type Binding = import("types/service").Binding + /** * @returns substitute icon || name || fallback icon */ export function icon(name: string | null, fallback = icons.missing) { + const validateSubstitute = (name: string): name is SubstituteKeys => name in substitutes; + if (!name) return fallback || "" if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name - const icon = (substitutes[name] || name) + let icon: string = name; + + if (validateSubstitute(name)) { + icon = substitutes[name]; + } + if (Utils.lookUpIcon(icon)) return icon diff --git a/modules/icons/index.ts b/modules/icons/index.ts index a573875..0678c1f 100644 --- a/modules/icons/index.ts +++ b/modules/icons/index.ts @@ -196,4 +196,4 @@ export default { patchy_light_snow_with_thunder: "weather-snow-symbolic", moderate_or_heavy_snow_with_thunder: "weather-snow-symbolic", }, -}; +} as const; diff --git a/modules/icons/weather.ts b/modules/icons/weather.ts index d3b6cbe..1a9bd49 100644 --- a/modules/icons/weather.ts +++ b/modules/icons/weather.ts @@ -51,4 +51,4 @@ export const weatherIcons = { moderate_or_heavy_rain_with_thunder: "󰙾", patchy_light_snow_with_thunder: "󰼶", moderate_or_heavy_snow_with_thunder: "󰼶", -}; +} as const; diff --git a/modules/menus/DropdownMenu.ts b/modules/menus/DropdownMenu.ts index 2c9cc18..34cc24d 100644 --- a/modules/menus/DropdownMenu.ts +++ b/modules/menus/DropdownMenu.ts @@ -1,4 +1,5 @@ const hyprland = await Service.import("hyprland"); +import { DropdownMenuProps } from "lib/types/dropdownmenu"; import { Exclusivity } from "lib/types/widget"; import { bash } from "lib/utils"; import { Monitor } from "types/service/hyprland"; @@ -99,15 +100,17 @@ setTimeout(() => { initRender.value = false; }, 2000); -export default ({ - name, - child, - layout = "center", - transition, - exclusivity = "ignore" as Exclusivity, - fixed = false, - ...props -}) => +export default ( + { + name, + child, + layout = "center", + transition, + exclusivity = "ignore" as Exclusivity, + fixed = false, + ...props + }: DropdownMenuProps +) => Widget.Window({ name, class_names: [name, "dropdown-menu"], diff --git a/modules/menus/PopupWindow.ts b/modules/menus/PopupWindow.ts index af26df4..dec90f2 100644 --- a/modules/menus/PopupWindow.ts +++ b/modules/menus/PopupWindow.ts @@ -1,3 +1,5 @@ +import { WINDOW_LAYOUTS } from "globals/window"; +import { LayoutFunction, Layouts, PopupWindowProps } from "lib/types/popupwindow"; import { Exclusivity, Transition } from "lib/types/widget"; type Opts = { @@ -32,7 +34,7 @@ const PopupRevealer = (name: string, child: any, transition = "slide_down" as Tr }), ); -const Layout = (name: string, child: any, transition: Transition) => ({ +const Layout: LayoutFunction = (name: string, child: any, transition: Transition) => ({ center: () => Widget.CenterBox( {}, @@ -150,6 +152,10 @@ const Layout = (name: string, child: any, transition: Transition) => ({ ), }); +const isValidLayout = (layout: string): layout is Layouts => { + return WINDOW_LAYOUTS.includes(layout); +}; + export default ({ name, child, @@ -157,8 +163,12 @@ export default ({ transition, exclusivity = "ignore" as Exclusivity, ...props -}) => - Widget.Window({ +}: PopupWindowProps) => { + const layoutFn = isValidLayout(layout) ? layout : "center"; + + const layoutWidget = Layout(name, child, transition)[layoutFn](); + + return Widget.Window({ name, class_names: [name, "popup-window"], setup: (w) => w.keybind("Escape", () => App.closeWindow(name)), @@ -167,6 +177,7 @@ export default ({ exclusivity, layer: "top", anchor: ["top", "bottom", "right", "left"], - child: Layout(name, child, transition)[layout](), + child: layoutWidget, ...props, }); +} diff --git a/modules/menus/audio/utils.ts b/modules/menus/audio/utils.ts index 2aa71a7..da505c3 100644 --- a/modules/menus/audio/utils.ts +++ b/modules/menus/audio/utils.ts @@ -1,27 +1,31 @@ -const getIcon = (audioVol, isMuted) => { - const speakerIcons = { +const speakerIcons = { 101: "audio-volume-overamplified-symbolic", 66: "audio-volume-high-symbolic", 34: "audio-volume-medium-symbolic", 1: "audio-volume-low-symbolic", 0: "audio-volume-muted-symbolic", - }; +} as const; - const inputIcons = { +const inputIcons = { + 101: "microphone-sensitivity-high-symbolic", 66: "microphone-sensitivity-high-symbolic", 34: "microphone-sensitivity-medium-symbolic", 1: "microphone-sensitivity-low-symbolic", 0: "microphone-disabled-symbolic", - }; +}; - const icon = isMuted - ? 0 - : [101, 66, 34, 1, 0].find((threshold) => threshold <= audioVol * 100); +type IconVolumes = keyof typeof speakerIcons; - return { - spkr: speakerIcons[icon], - mic: inputIcons[icon], - }; +const getIcon = (audioVol: IconVolumes, isMuted: boolean) => { + const thresholds: IconVolumes[] = [101, 66, 34, 1, 0]; + const icon = isMuted + ? 0 + : thresholds.find((threshold) => threshold <= audioVol * 100) || 0; + + return { + spkr: speakerIcons[icon], + mic: inputIcons[icon], + }; }; export { getIcon }; diff --git a/modules/menus/bluetooth/devices/devicelist.ts b/modules/menus/bluetooth/devices/devicelist.ts index 6827145..52bbe30 100644 --- a/modules/menus/bluetooth/devices/devicelist.ts +++ b/modules/menus/bluetooth/devices/devicelist.ts @@ -88,7 +88,7 @@ const devices = (bluetooth: Bluetooth, self: Box) => { Widget.Label({ vpack: "start", class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? "active" : ""} txt-icon`, - label: getBluetoothIcon(`${device["icon-name"]}-symbolic`), + label: getBluetoothIcon(`${device["icon_name"]}-symbolic`), }), Widget.Box({ vertical: true, diff --git a/modules/menus/calendar/weather/hourly/icon/index.ts b/modules/menus/calendar/weather/hourly/icon/index.ts index f3b9cd7..1a3e31c 100644 --- a/modules/menus/calendar/weather/hourly/icon/index.ts +++ b/modules/menus/calendar/weather/hourly/icon/index.ts @@ -1,35 +1,43 @@ -import { Weather } from "lib/types/weather.js"; +import { Weather, WeatherIcon, WeatherIconTitle } from "lib/types/weather.js"; import { Variable } from "types/variable.js"; import { weatherIcons } from "modules/icons/weather.js"; +import { isValidWeatherIconTitle } from "globals/weather"; export const HourlyIcon = (theWeather: Variable, getNextEpoch: any) => { - const getIconQuery = (wthr: Weather) => { + const getIconQuery = (wthr: Weather): WeatherIconTitle => { const nextEpoch = getNextEpoch(wthr); const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find( (h) => h.time_epoch === nextEpoch, ); - let iconQuery = weatherAtEpoch?.condition.text + if (weatherAtEpoch === undefined) { + return "warning"; + } + + let iconQuery = weatherAtEpoch.condition.text .trim() .toLowerCase() - .replaceAll(" ", "_") - || "warning" - ; + .replaceAll(" ", "_"); if (!weatherAtEpoch?.is_day && iconQuery === "partly_cloudy") { iconQuery = "partly_cloudy_night"; } - return iconQuery; + + if (isValidWeatherIconTitle(iconQuery)) { + return iconQuery; + } else { + return "warning"; + } } return Widget.Box({ hpack: "center", child: theWeather.bind("value").as((w) => { - let weatherIcn = "-"; + let weatherIcn: WeatherIcon; const iconQuery = getIconQuery(w); - weatherIcn = weatherIcons[iconQuery]; + weatherIcn = weatherIcons[iconQuery] || weatherIcons["warning"]; return Widget.Label({ hpack: "center", class_name: "hourly-weather-icon txt-icon", diff --git a/modules/menus/dashboard/stats/index.ts b/modules/menus/dashboard/stats/index.ts index 05fa1f9..37bfdbe 100644 --- a/modules/menus/dashboard/stats/index.ts +++ b/modules/menus/dashboard/stats/index.ts @@ -6,7 +6,7 @@ const { terminal } = options; const { enable_gpu } = options.menus.dashboard.stats; const Stats = () => { - const divide = ([total, free]) => free / total; + const divide = ([total, free]: number[]) => free / total; const formatSizeInGB = (sizeInKB: number) => Number((sizeInKB / 1024 ** 2).toFixed(2)); @@ -26,7 +26,8 @@ const Stats = () => { return 0; } - return divide([100, cpuOut.split(/\s+/)[1].replace(",", ".")]); + const freeCpu = parseFloat(cpuOut.split(/\s+/)[1].replace(",", ".")); + return divide([100, freeCpu]); }, ], }); diff --git a/modules/menus/energy/profiles/index.ts b/modules/menus/energy/profiles/index.ts index 80ba93f..f13dad3 100644 --- a/modules/menus/energy/profiles/index.ts +++ b/modules/menus/energy/profiles/index.ts @@ -1,7 +1,11 @@ const powerProfiles = await Service.import("powerprofiles"); +import { PowerProfile, PowerProfileObject, PowerProfiles } from "lib/types/powerprofiles.js"; import icons from "../../../icons/index.js"; const EnergyProfiles = () => { + const isValidProfile = (profile: string): profile is PowerProfile => + profile === "power-saver" || profile === "balanced" || profile === "performance"; + return Widget.Box({ class_name: "menu-section-container energy", vertical: true, @@ -21,13 +25,20 @@ const EnergyProfiles = () => { vpack: "fill", vexpand: true, vertical: true, - children: powerProfiles.bind("profiles").as((profiles) => { - return profiles.map((prof) => { - const ProfileLabels = { + children: powerProfiles.bind("profiles").as((profiles: PowerProfiles) => { + return profiles.map((prof: PowerProfileObject) => { + const profileLabels = { "power-saver": "Power Saver", balanced: "Balanced", performance: "Performance", }; + + const profileType = prof.Profile; + + if (!isValidProfile(profileType)) { + return profileLabels.balanced; + } + return Widget.Button({ on_primary_click: () => { powerProfiles.active_profile = prof.Profile; @@ -39,11 +50,11 @@ const EnergyProfiles = () => { children: [ Widget.Icon({ class_name: "power-profile-icon", - icon: icons.powerprofile[prof.Profile], + icon: icons.powerprofile[profileType], }), Widget.Label({ class_name: "power-profile-label", - label: ProfileLabels[prof.Profile], + label: profileLabels[profileType], }), ], }), diff --git a/modules/menus/media/components/controls.ts b/modules/menus/media/components/controls.ts index 54471f5..f31ac53 100644 --- a/modules/menus/media/components/controls.ts +++ b/modules/menus/media/components/controls.ts @@ -1,17 +1,24 @@ import { MprisPlayer } from "types/service/mpris.js"; import icons from "../../../icons/index.js"; +import { LoopStatus, PlaybackStatus } from "lib/types/mpris.js"; const media = await Service.import("mpris"); const Controls = (getPlayerInfo: Function) => { + const isValidLoopStatus = (status: string): status is LoopStatus => + ["none", "track", "playlist"].includes(status); + + const isValidPlaybackStatus = (status: string): status is PlaybackStatus => + ["playing", "paused", "stopped"].includes(status); + const isLoopActive = (player: MprisPlayer) => { - return player["loop-status"] !== null && - ["track", "playlist"].includes(player["loop-status"].toLowerCase()) + return player["loop_status"] !== null && + ["track", "playlist"].includes(player["loop_status"].toLowerCase()) ? "active" : ""; }; const isShuffleActive = (player: MprisPlayer) => { - return player["shuffle-status"] !== null && player["shuffle-status"] + return player["shuffle_status"] !== null && player["shuffle_status"] ? "active" : ""; }; @@ -98,13 +105,18 @@ const Controls = (getPlayerInfo: Function) => { media, "changed", () => { - const foundPlayer = getPlayerInfo(); + const foundPlayer: MprisPlayer = getPlayerInfo(); if (foundPlayer === undefined) { return icons.mpris["paused"]; } - return icons.mpris[ - foundPlayer.play_back_status.toLowerCase() - ]; + const playbackStatus = foundPlayer.play_back_status?.toLowerCase(); + + if (playbackStatus && isValidPlaybackStatus(playbackStatus)) { + return icons.mpris[playbackStatus]; + } + else { + return icons.mpris["paused"]; + } }, ), }), @@ -161,18 +173,21 @@ const Controls = (getPlayerInfo: Function) => { child: Widget.Icon({ setup: (self) => { self.hook(media, () => { - const foundPlayer = getPlayerInfo(); + const foundPlayer: MprisPlayer = getPlayerInfo(); + if (foundPlayer === undefined) { self.icon = icons.mpris.loop["none"]; return; } - self.icon = - foundPlayer.loop_status === null - ? icons.mpris.loop["none"] - : icons.mpris.loop[ - foundPlayer.loop_status?.toLowerCase() - ]; + const loopStatus = foundPlayer.loop_status?.toLowerCase(); + + if (loopStatus && isValidLoopStatus(loopStatus)) { + self.icon = icons.mpris.loop[loopStatus]; + } + else { + self.icon = icons.mpris.loop["none"]; + } }); }, }), diff --git a/modules/menus/media/media.ts b/modules/menus/media/media.ts index a0ea66d..6513b4c 100644 --- a/modules/menus/media/media.ts +++ b/modules/menus/media/media.ts @@ -37,17 +37,17 @@ const Media = () => { }; const isPlaying = media.players.find( - (p) => p["play-back-status"] === "Playing", + (p) => p["play_back_status"] === "Playing", ); const playerStillExists = media.players.some( - (p) => curPlayer.value === p["bus-name"], + (p) => curPlayer.value === p["bus_name"], ); const nextPlayerUp = media.players.sort( (a, b) => - statusOrder[a["play-back-status"]] - - statusOrder[b["play-back-status"]], + statusOrder[a["play_back_status"]] - + statusOrder[b["play_back_status"]], )[0].bus_name; if (isPlaying || !playerStillExists) { diff --git a/modules/menus/network/wifi/WirelessAPs.ts b/modules/menus/network/wifi/WirelessAPs.ts index 5bd65c9..9655c1e 100644 --- a/modules/menus/network/wifi/WirelessAPs.ts +++ b/modules/menus/network/wifi/WirelessAPs.ts @@ -1,7 +1,8 @@ import { Network } from "types/service/network.js"; -import { AccessPoint } from "lib/types/network.js"; +import { AccessPoint, WifiStatus } from "lib/types/network.js"; import { Variable } from "types/variable.js"; import { getWifiIcon } from "../utils.js"; +import { WIFI_STATUS_MAP } from "globals/network.js"; const renderWAPs = (self: any, network: Network, staging: Variable, connecting: Variable) => { const getIdBySsid = (ssid: string, nmcliOutput: string) => { const lines = nmcliOutput.trim().split("\n"); @@ -14,33 +15,36 @@ const renderWAPs = (self: any, network: Network, staging: Variable, return null; }; - const WifiStatusMap = { - unknown: "Status Unknown", - unmanaged: "Unmanaged", - unavailable: "Unavailable", - disconnected: "Disconnected", - prepare: "Preparing Connecting", - config: "Connecting", - need_auth: "Needs Authentication", - ip_config: "Requesting IP", - ip_check: "Checking Access", - secondaries: "Waiting on Secondaries", - activated: "Connected", - deactivating: "Disconnecting", - failed: "Connection Failed", + const isValidWifiStatus = (status: string): status is WifiStatus => { + return status in WIFI_STATUS_MAP; }; + + + const getWifiStatus = () => { + const wifiState = network.wifi.state?.toLowerCase(); + + if (wifiState && isValidWifiStatus(wifiState)) { + return WIFI_STATUS_MAP[wifiState]; + } + return WIFI_STATUS_MAP["unknown"]; + } + self.hook(network, () => { Utils.merge([staging.bind("value"), connecting.bind("value")], () => { - // Sometimes the network service will yield a "this._device is undefined" when + // NOTE: Sometimes the network service will yield a "this._device is undefined" when // trying to access the "access_points" property. So we must validate that // it's not 'undefined' - // + // -- // Also this is an AGS bug that needs to be fixed - let WAPs = - network.wifi._device !== undefined ? network.wifi["access-points"] : []; + + // TODO: Remove @ts-ignore once AGS bug is fixed + // @ts-ignore + let WAPs = network.wifi._device !== undefined + ? network.wifi["access_points"] + : []; const dedupeWAPs = () => { - const dedupMap = {}; + const dedupMap: Record = {}; WAPs.forEach((item: AccessPoint) => { if (item.ssid !== null && !Object.hasOwnProperty.call(dedupMap, item.ssid)) { dedupMap[item.ssid] = item; @@ -153,8 +157,7 @@ const renderWAPs = (self: any, network: Network, staging: Variable, child: Widget.Label({ hpack: "start", class_name: "connection-status dim", - label: - WifiStatusMap[network.wifi.state.toLowerCase()], + label: getWifiStatus() }), }), ], diff --git a/modules/menus/notifications/notification/header/icon.ts b/modules/menus/notifications/notification/header/icon.ts index af6162b..9b6503c 100644 --- a/modules/menus/notifications/notification/header/icon.ts +++ b/modules/menus/notifications/notification/header/icon.ts @@ -1,32 +1,18 @@ -import icons from "../../../../icons/index.js"; +import { Notification } from "types/service/notifications.js"; +import { NotificationIcon } from "lib/types/notification.js"; +import { getNotificationIcon } from "globals/notification"; -const NotificationIcon = ({ app_entry, app_icon, app_name }) => { - let icon = icons.fallback.notification; - - if ( - Utils.lookUpIcon(app_name) || - Utils.lookUpIcon(app_name.toLowerCase() || "") - ) - icon = Utils.lookUpIcon(app_name) - ? app_name - : Utils.lookUpIcon(app_name.toLowerCase()) - ? app_name.toLowerCase() - : ""; - - if (Utils.lookUpIcon(app_icon) && icon === "") icon = app_icon; - - if (Utils.lookUpIcon(app_entry || "") && icon === "") icon = app_entry || ""; - - return Widget.Box({ - css: ` +const NotificationIcon = ({ app_entry = "", app_icon = "", app_name = "" }: Partial) => { + return Widget.Box({ + css: ` min-width: 2rem; min-height: 2rem; `, - child: Widget.Icon({ - class_name: "notification-icon menu", - icon, - }), - }); + child: Widget.Icon({ + class_name: "notification-icon menu", + icon: getNotificationIcon(app_name, app_icon, app_entry), + }), + }); }; export { NotificationIcon }; diff --git a/modules/menus/notifications/pager/index.ts b/modules/menus/notifications/pager/index.ts index 92dceb7..08aaca9 100644 --- a/modules/menus/notifications/pager/index.ts +++ b/modules/menus/notifications/pager/index.ts @@ -10,7 +10,7 @@ export const NotificationPager = (curPage: Variable) => { class_name: "notification-menu-pager", hexpand: true, vexpand: false, - children: Utils.merge([curPage.bind("value"), displayedTotal.bind("value"), notifs.bind("notifications")], (currentPage, dispTotal, notifications) => { + children: Utils.merge([curPage.bind("value"), displayedTotal.bind("value"), notifs.bind("notifications")], (currentPage, dispTotal, _) => { return [ Widget.Button({ hexpand: true, diff --git a/modules/menus/powerDropdown/button.ts b/modules/menus/powerDropdown/button.ts index 14d7e8b..729c159 100644 --- a/modules/menus/powerDropdown/button.ts +++ b/modules/menus/powerDropdown/button.ts @@ -1,4 +1,3 @@ -import icons from "lib/icons"; import { PowerOptions } from "lib/types/options"; import options from "options"; import powermenu from "../power/helpers/actions"; diff --git a/modules/menus/powerDropdown/index.ts b/modules/menus/powerDropdown/index.ts index ec56353..2226b48 100644 --- a/modules/menus/powerDropdown/index.ts +++ b/modules/menus/powerDropdown/index.ts @@ -1,9 +1,6 @@ -import options from "options.js"; import DropdownMenu from "../DropdownMenu.js"; import { PowerButton } from "./button.js"; -const { showLabel } = options.menus.power; - export default () => { return DropdownMenu({ name: "powerdropdownmenu", diff --git a/modules/notifications/header/icon.ts b/modules/notifications/header/icon.ts index 7f53111..1c706d6 100644 --- a/modules/notifications/header/icon.ts +++ b/modules/notifications/header/icon.ts @@ -1,32 +1,17 @@ -import icons from "../../icons/index.js"; +import { Notification } from "types/service/notifications.js"; +import { getNotificationIcon } from "globals/notification.js"; -const NotificationIcon = ({ app_entry, app_icon, app_name }) => { - let icon = icons.fallback.notification; - - if ( - Utils.lookUpIcon(app_name) || - Utils.lookUpIcon(app_name.toLowerCase() || "") - ) - icon = Utils.lookUpIcon(app_name) - ? app_name - : Utils.lookUpIcon(app_name.toLowerCase()) - ? app_name.toLowerCase() - : ""; - - if (Utils.lookUpIcon(app_icon) && icon === "") icon = app_icon; - - if (Utils.lookUpIcon(app_entry || "") && icon === "") icon = app_entry || ""; - - return Widget.Box({ - css: ` +const NotificationIcon = ({ app_entry = "", app_icon = "", app_name = "" }: Partial) => { + return Widget.Box({ + css: ` min-width: 2rem; min-height: 2rem; `, - child: Widget.Icon({ - class_name: "notification-icon", - icon, - }), - }); + child: Widget.Icon({ + class_name: "notification-icon", + icon: getNotificationIcon(app_name, app_icon, app_entry) + }), + }); }; export { NotificationIcon }; diff --git a/modules/osd/icon/index.ts b/modules/osd/icon/index.ts index e991fe6..7ff516a 100644 --- a/modules/osd/icon/index.ts +++ b/modules/osd/icon/index.ts @@ -1,8 +1,7 @@ -import { OSDOrientation } from "lib/types/options"; import brightness from "services/Brightness" const audio = await Service.import("audio") -export const OSDIcon = (ort: OSDOrientation) => { +export const OSDIcon = () => { return Widget.Box({ class_name: "osd-icon-container", hexpand: true, diff --git a/modules/osd/index.ts b/modules/osd/index.ts index cc9a6b9..6ee71d8 100644 --- a/modules/osd/index.ts +++ b/modules/osd/index.ts @@ -9,6 +9,7 @@ const audio = await Service.import("audio") const { enable, + duration, orientation, location, active_monitor, @@ -21,8 +22,6 @@ hyprland.active.connect("changed", () => { curMonitor.value = hyprland.active.monitor.id; }) -const DELAY = 2500; - let count = 0 const handleReveal = (self: any, property: string) => { if (!enable.value) { @@ -30,7 +29,7 @@ const handleReveal = (self: any, property: string) => { } self[property] = true count++ - Utils.timeout(DELAY, () => { + Utils.timeout(duration.value, () => { count-- if (count === 0) @@ -39,8 +38,6 @@ const handleReveal = (self: any, property: string) => { } const renderOSD = () => { - - return Widget.Revealer({ transition: "crossfade", reveal_child: false, @@ -72,16 +69,16 @@ const renderOSD = () => { children: orientation.bind("value").as(ort => { if (ort === "vertical") { return [ - OSDLabel(ort), + OSDLabel(), OSDBar(ort), - OSDIcon(ort) + OSDIcon() ] } return [ - OSDIcon(ort), + OSDIcon(), OSDBar(ort), - OSDLabel(ort), + OSDLabel(), ] }) }) diff --git a/modules/osd/label/index.ts b/modules/osd/label/index.ts index 33fc923..72077c8 100644 --- a/modules/osd/label/index.ts +++ b/modules/osd/label/index.ts @@ -1,9 +1,8 @@ -import { OSDOrientation } from "lib/types/options"; import brightness from "services/Brightness" import options from "options" const audio = await Service.import("audio") -export const OSDLabel = (ort: OSDOrientation) => { +export const OSDLabel = () => { return Widget.Box({ class_name: "osd-label-container", hexpand: true, diff --git a/options.ts b/options.ts index c15d66f..60904fd 100644 --- a/options.ts +++ b/options.ts @@ -109,6 +109,7 @@ const options = mkOptions(OPTIONS, { }, osd: { scaling: opt(100), + duration: opt(2500), enable: opt(true), orientation: opt("vertical"), opacity: opt(100), diff --git a/scss/style.ts b/scss/style.ts index 46885e0..e8f5f62 100644 --- a/scss/style.ts +++ b/scss/style.ts @@ -1,8 +1,10 @@ import options from "options"; import { bash, dependencies } from "lib/utils"; -import { MatugenColors } from "lib/types/options"; +import { MatugenColors, RecursiveOptionsObject } from "lib/types/options"; import { initializeTrackers } from "./options_trackers"; import { generateMatugenColors, replaceHexValues } from "../services/matugen/index"; +import { isHexColor, isOpt, isRecursiveOptionsObject } from "globals/variables"; +import { Opt } from "lib/option"; const deps = [ "font", @@ -13,27 +15,37 @@ const deps = [ "bar.battery.blocks", ]; -function extractVariables(theme: typeof options.theme, prefix = "", matugenColors: MatugenColors | undefined) { +function extractVariables( + theme: RecursiveOptionsObject, + prefix = "", + matugenColors?: MatugenColors +): string[] { let result = [] as string[]; for (let key in theme) { - if (theme.hasOwnProperty(key)) { - const value = theme[key]; + if (!theme.hasOwnProperty(key)) { + continue; + } - const newPrefix = prefix ? `${prefix}-${key}` : key; + const value = theme[key]; - const isColor = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value.value); - const replacedValue = isColor && matugenColors !== undefined ? replaceHexValues(value.value, matugenColors) : value.value; - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { - if (typeof value.value !== 'undefined') { - result.push(`$${newPrefix}: ${replacedValue};`); - } else { - result = result.concat(extractVariables(value, newPrefix, matugenColors)); - } - } else if (typeof value === 'function' && value.name === 'opt') { - result.push(`$${newPrefix}: ${replacedValue};`); - } + const newPrefix = prefix ? `${prefix}-${key}` : key; + + const isColor = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value.value); + const replacedValue = isColor && matugenColors !== undefined ? replaceHexValues(value.value, matugenColors) : value.value; + + if (typeof value === 'function') { + result.push(`$${newPrefix}: ${replacedValue};`); + continue; + } + if (typeof value !== 'object' || value === null || Array.isArray(value)) continue; + + if (typeof value.value !== 'undefined') { + result.push(`$${newPrefix}: ${replacedValue};`); + } else { + result = result.concat(extractVariables(value as RecursiveOptionsObject, newPrefix, matugenColors)); } } + return result; } diff --git a/scss/style/osd/index.scss b/scss/style/osd/index.scss index 63a1100..8b60d26 100644 --- a/scss/style/osd/index.scss +++ b/scss/style/osd/index.scss @@ -31,8 +31,8 @@ border-radius: if($osd-orientation =="vertical", 0em 0em $osd-radius $osd-radius, $osd-radius 0em 0em $osd-radius ); .osd-icon { - font-size: 2em; - padding: if($osd-orientation =="vertical", 0.2em 0em, 0em 0.2em); + font-size: 2.1em; + padding: if($osd-orientation =="vertical", 0.2em 0em, 0em 0.4em); color: $osd-icon; } } diff --git a/tsconfig.json b/tsconfig.json index 288c831..7a6ec83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "noEmit": true, "allowImportingTsExtensions": true, "target": "ES2022", "module": "ES2022", @@ -10,11 +11,14 @@ "checkJs": true, "strict": true, "noImplicitAny": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noImplicitThis": true, "baseUrl": ".", "typeRoots": [ "types", "lib/types/globals", ], - "skipLibCheck": true + "skipLibCheck": true, } } diff --git a/widget/settings/pages/config/osd/index.ts b/widget/settings/pages/config/osd/index.ts index e9a7f9e..611d813 100644 --- a/widget/settings/pages/config/osd/index.ts +++ b/widget/settings/pages/config/osd/index.ts @@ -12,6 +12,7 @@ export const OSDSettings = () => { children: [ Header('On Screen Display'), Option({ opt: options.theme.osd.enable, title: 'Enabled', type: 'boolean' }), + Option({ opt: options.theme.osd.duration, title: 'Duration', type: 'number', min: 100, max: 10000, increment: 500 }), Option({ opt: options.theme.osd.orientation, title: 'Orientation', type: 'enum', enums: ["horizontal", "vertical"] }), Option({ opt: options.theme.osd.location, title: 'Position', subtitle: 'Position of the OSD on the screen', type: 'enum', enums: ["top left", "top", "top right", "right", "bottom right", "bottom", "bottom left", "left"] }), Option({ opt: options.theme.osd.monitor, title: 'Monitor', subtitle: 'The ID of the monitor on which to display the OSD', type: 'number' }), diff --git a/widget/settings/shared/FileChooser.ts b/widget/settings/shared/FileChooser.ts index 49ea131..7bebba5 100644 --- a/widget/settings/shared/FileChooser.ts +++ b/widget/settings/shared/FileChooser.ts @@ -2,14 +2,15 @@ import Gtk from "gi://Gtk?version=3.0"; import Gio from "gi://Gio" import { bash, Notify } from "lib/utils"; import icons from "lib/icons" +import { Config } from "lib/types/filechooser"; +import { hexColorPattern } from "globals/useTheme"; +import { isHexColor } from "globals/variables"; const whiteListedThemeProp = [ "theme.bar.buttons.style" ]; - -// Helper functions -export const loadJsonFile = (filePath: string): object | null => { +export const loadJsonFile = (filePath: string): Config | null => { let file = Gio.File.new_for_path(filePath as string); let [success, content] = file.load_contents(null); @@ -32,10 +33,8 @@ export const saveConfigToFile = (config: object, filePath: string): void => { dataOutputStream.close(null); } -export const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; - -export const filterConfigForThemeOnly = (config: object) => { - let filteredConfig = {}; +export const filterConfigForThemeOnly = (config: Config): Config => { + let filteredConfig: Config = {}; for (let key in config) { if (typeof config[key] === 'string' && hexColorPattern.test(config[key])) { @@ -47,8 +46,8 @@ export const filterConfigForThemeOnly = (config: object) => { return filteredConfig; }; -export const filterConfigForNonTheme = (config: object) => { - let filteredConfig = {}; +export const filterConfigForNonTheme = (config: Config): Config => { + let filteredConfig: Config = {}; for (let key in config) { if (whiteListedThemeProp.includes(key)) { continue; @@ -75,12 +74,11 @@ export const saveFileDialog = (filePath: string, themeOnly: boolean): void => { let jsonObject = JSON.parse(jsonString); // Function to filter hex color pairs - const filterHexColorPairs = (jsonObject: object) => { - const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; - let filteredObject = {}; + const filterHexColorPairs = (jsonObject: Config) => { + let filteredObject: Config = {}; for (let key in jsonObject) { - if (typeof jsonObject[key] === 'string' && hexColorPattern.test(jsonObject[key])) { + if (typeof jsonObject[key] === 'string' && isHexColor(jsonObject[key])) { filteredObject[key] = jsonObject[key]; } else if (whiteListedThemeProp.includes(key)) { filteredObject[key] = jsonObject[key]; @@ -92,9 +90,8 @@ export const saveFileDialog = (filePath: string, themeOnly: boolean): void => { }; // Function to filter out hex color pairs (keep only non-hex color value) - const filterOutHexColorPairs = (jsonObject: object) => { - const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; - let filteredObject = {}; + const filterOutHexColorPairs = (jsonObject: Config) => { + let filteredObject: Config = {}; for (let key in jsonObject) { // do not add key-value pair if its in whiteListedThemeProp @@ -102,7 +99,7 @@ export const saveFileDialog = (filePath: string, themeOnly: boolean): void => { continue; } - if (!(typeof jsonObject[key] === 'string' && hexColorPattern.test(jsonObject[key]))) { + if (!(typeof jsonObject[key] === 'string' && isHexColor(jsonObject[key]))) { filteredObject[key] = jsonObject[key]; } } @@ -192,8 +189,19 @@ export const importFiles = (themeOnly: boolean = false): void => { return; } if (response === Gtk.ResponseType.ACCEPT) { - let filePath = dialog.get_filename(); - let importedConfig = loadJsonFile(filePath as string); + let filePath: string | null = dialog.get_filename(); + + if (filePath === null) { + Notify({ + summary: "Failed to import", + body: "No file selected.", + iconName: icons.ui.warning, + timeout: 5000 + }); + return; + } + + let importedConfig = loadJsonFile(filePath); if (!importedConfig) { dialog.destroy();