Added strict type checking to the project. (#236)

* Implement strict typing (WIP).

* changes

* Finish type checks

* Fix notification icon, matugen settings and update tsconfig.

* OSD Styling updates and added the ability to configure OSD duration.
This commit is contained in:
Jas Singh
2024-09-09 00:44:51 -07:00
committed by GitHub
parent 41dbc3829a
commit bb3b3dfdfb
56 changed files with 468 additions and 240 deletions

View File

@@ -69,7 +69,6 @@ export const pollVariableBash = <T>(
}); });
}; };
// Set up the interval initially with the provided polling interval
Utils.merge([pollingInterval, ...trackers], (pollIntrvl: number) => { Utils.merge([pollingInterval, ...trackers], (pollIntrvl: number) => {
intervalFn(pollIntrvl); intervalFn(pollIntrvl);
}); });

View File

@@ -1,8 +1,6 @@
// @ts-expect-error // @ts-expect-error
import GTop from 'gi://GTop'; import GTop from 'gi://GTop';
const defaultCpuData: number = 0;
let previousCpuData = new GTop.glibtop_cpu(); let previousCpuData = new GTop.glibtop_cpu();
GTop.glibtop_get_cpu(previousCpuData); GTop.glibtop_get_cpu(previousCpuData);

View File

@@ -1,8 +1,5 @@
import options from "options"; import options from "options";
// @ts-expect-error
import GTop from 'gi://GTop';
// Module initializer // Module initializer
import { module } from "../module" import { module } from "../module"

View File

@@ -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"; import { layoutMap } from "./layouts";
export const getKeyboardLayout = (obj: string, format: KbLabelType) => { export const getKeyboardLayout = (obj: string, format: KbLabelType) => {
@@ -15,7 +15,8 @@ export const getKeyboardLayout = (obj: string, format: KbLabelType) => {
mainKb = keyboards[keyboards.length - 1]; 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;
} }

View File

@@ -581,4 +581,4 @@ export const layoutMap = {
"Wolof": "SN", "Wolof": "SN",
"Yakut": "RU (Sah)", "Yakut": "RU (Sah)",
"Yoruba": "NG (Yoruba)" "Yoruba": "NG (Yoruba)"
}; } as const;

View File

@@ -1,4 +1,5 @@
import { Module } from "lib/types/bar"; import { Module } from "lib/types/bar";
import { BarButtonStyles } from "lib/types/options";
import options from "options"; import options from "options";
import Gtk from "types/@girs/gtk-3.0/gtk-3.0"; import Gtk from "types/@girs/gtk-3.0/gtk-3.0";
import { Binding } from "types/service"; import { Binding } from "types/service";
@@ -40,7 +41,7 @@ export const module = ({
return { return {
component: Widget.Box({ 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 shouldShowLabel = shwLabel || showLabel;
const styleMap = { const styleMap = {
default: "style1", default: "style1",

View File

@@ -1,5 +1,6 @@
import { ResourceLabelType } from 'lib/types/bar'; import { ResourceLabelType } from 'lib/types/bar';
import { GenericResourceData } from 'lib/types/customModules/generic'; import { GenericResourceData } from 'lib/types/customModules/generic';
import { InputHandlerEvents } from 'lib/types/customModules/utils';
import { Binding } from 'lib/utils'; import { Binding } from 'lib/utils';
import { openMenu } from 'modules/bar/utils'; import { openMenu } from 'modules/bar/utils';
import options from 'options'; import options from 'options';
@@ -75,7 +76,7 @@ export const inputHandler = (
onMiddleClick, onMiddleClick,
onScrollUp, onScrollUp,
onScrollDown, onScrollDown,
} }: InputHandlerEvents
) => { ) => {
const sanitizeInput = (input: VariableType<string>): string => { const sanitizeInput = (input: VariableType<string>): string => {
if (input === undefined) { if (input === undefined) {

5
globals.d.ts vendored
View File

@@ -1,9 +1,12 @@
// globals.d.ts // globals.d.ts
import { Variable as VariableType } from "types/variable"; import { Options, Variable as VariableType } from "types/variable";
declare global { declare global {
var globalMousePos: VariableType<number[]>; var globalMousePos: VariableType<number[]>;
var useTheme: Function;
var globalWeatherVar: VariableType<Weather>;
var options: Options
} }
export { }; export { };

16
globals/network.ts Normal file
View File

@@ -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;

24
globals/notification.ts Normal file
View File

@@ -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;
};

View File

@@ -3,6 +3,8 @@ import { bash, Notify } from "lib/utils";
import icons from "lib/icons" import icons from "lib/icons"
import { filterConfigForThemeOnly, loadJsonFile, saveConfigToFile } from "widget/settings/shared/FileChooser"; 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 => { globalThis.useTheme = (filePath: string): void => {
let importedConfig = loadJsonFile(filePath); let importedConfig = loadJsonFile(filePath);

13
globals/variables.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Opt } from "lib/option";
import { HexColor, RecursiveOptionsObject } from "lib/types/options";
export const isOpt = <T>(value: unknown): value is Opt<T> =>
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);
}

View File

@@ -1,9 +1,7 @@
import options from "options"; 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 { DEFAULT_WEATHER } from "lib/types/defaults/weather.js";
import GLib from "gi://GLib?version=2.0" import GLib from "gi://GLib?version=2.0";
import icons from "../modules/icons/index.js";
import { weatherIcons } from "modules/icons/weather.js"; import { weatherIcons } from "modules/icons/weather.js";
const { key, interval, location } = options.menus.clock.weather; const { key, interval, location } = options.menus.clock.weather;
@@ -26,16 +24,16 @@ const weatherIntervalFn = (weatherInterval: number, loc: string, weatherKey: str
.then((res) => { .then((res) => {
try { try {
if (typeof res !== "string") { if (typeof res !== "string") {
return globalWeatherVar.value = DEFAULT_WEATHER; return (globalWeatherVar.value = DEFAULT_WEATHER);
} }
const parsedWeather = JSON.parse(res); const parsedWeather = JSON.parse(res);
if (Object.keys(parsedWeather).includes("error")) { 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) { } catch (error) {
globalWeatherVar.value = DEFAULT_WEATHER; globalWeatherVar.value = DEFAULT_WEATHER;
console.warn(`Failed to parse weather data: ${error}`); 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}`); console.error(`Failed to fetch weather: ${err}`);
globalWeatherVar.value = DEFAULT_WEATHER; globalWeatherVar.value = DEFAULT_WEATHER;
}); });
}) });
}; };
Utils.merge([key.bind("value"), interval.bind("value"), location.bind("value")], (weatherKey, weatherInterval, loc) => { Utils.merge([key.bind("value"), interval.bind("value"), location.bind("value")], (weatherKey, weatherInterval, loc) => {
if (!weatherKey) { if (!weatherKey) {
return globalWeatherVar.value = DEFAULT_WEATHER; return (globalWeatherVar.value = DEFAULT_WEATHER);
} }
weatherIntervalFn(weatherInterval, loc, weatherKey); weatherIntervalFn(weatherInterval, loc, weatherKey);
}); });
@@ -70,23 +68,28 @@ export const getWeatherIcon = (fahren: number) => {
50: "", 50: "",
25: "", 25: "",
0: "", 0: "",
}; } as const;
const colors = { const colors = {
100: "weather-color red", 100: "weather-color red",
75: "weather-color orange", 75: "weather-color orange",
50: "weather-color lavender", 50: "weather-color lavender",
25: "weather-color blue", 25: "weather-color blue",
0: "weather-color sky", 0: "weather-color sky",
}; } as const;
const threshold = type IconKeys = keyof typeof icons;
const threshold: IconKeys =
fahren < 0 fahren < 0
? 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 { return {
icon: icons[threshold || 50], icon,
color: colors[threshold || 50], 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_mph)} mph`;
} }
return `${Math.floor(wthr.current.wind_kph)} kph`; return `${Math.floor(wthr.current.wind_kph)} kph`;
} };
export const getRainChance = (wthr: Weather) => `${wthr.forecast.forecastday[0].day.daily_chance_of_rain}%`; 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 let iconQuery = wthr.current.condition.text
.trim() .trim()
.toLowerCase() .toLowerCase()
@@ -108,7 +115,14 @@ export const getWeatherStatusTextIcon = (wthr: Weather) => {
if (!wthr.current.is_day && iconQuery === "partly_cloudy") { if (!wthr.current.is_day && iconQuery === "partly_cloudy") {
iconQuery = "partly_cloudy_night"; 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; globalThis["globalWeatherVar"] = globalWeatherVar;

10
globals/window.ts Normal file
View File

@@ -0,0 +1,10 @@
export const WINDOW_LAYOUTS: string[] = [
'center',
'top',
'top-right',
'top-center',
'top-left',
'bottom-left',
'bottom-center',
'bottom-right'
];

View File

@@ -10,7 +10,7 @@ export const substitutes = {
"preferences-system": "emblem-system-symbolic", "preferences-system": "emblem-system-symbolic",
"com.github.Aylur.ags-symbolic": "controls-symbolic", "com.github.Aylur.ags-symbolic": "controls-symbolic",
"com.github.Aylur.ags": "controls-symbolic", "com.github.Aylur.ags": "controls-symbolic",
} } as const;
export default { export default {
missing: "image-missing-symbolic", missing: "image-missing-symbolic",

View File

@@ -1,3 +1,4 @@
import { isHexColor } from "globals/variables"
import { Variable } from "resource:///com/github/Aylur/ags/variable.js" import { Variable } from "resource:///com/github/Aylur/ags/variable.js"
type OptProps = { type OptProps = {
@@ -49,7 +50,7 @@ export class Opt<T = unknown> extends Variable<T> {
if (this.persistent) if (this.persistent)
return; 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) { if ((JSON.stringify(this.value) !== JSON.stringify(this.initial)) && isColor) {
this.value = this.initial this.value = this.initial
return this.id return this.id
@@ -60,35 +61,37 @@ export class Opt<T = unknown> extends Variable<T> {
export const opt = <T>(initial: T, opts?: OptProps) => new Opt(initial, opts) export const opt = <T>(initial: T, opts?: OptProps) => new Opt(initial, opts)
function getOptions(object: object, path = ""): Opt[] { function getOptions(object: Record<string, unknown>, path = ""): Opt[] {
return Object.keys(object).flatMap(key => { return Object.keys(object).flatMap(key => {
const obj: Opt = object[key] const obj = object[key];
const id = path ? path + "." + key : key const id = path ? path + "." + key : key;
if (obj instanceof Variable) { if (obj instanceof Variable) {
obj.id = id const optValue = obj as Opt;
return obj optValue.id = id;
return optValue;
} }
if (typeof obj === "object") if (typeof obj === "object" && obj !== null) {
return getOptions(obj, id) return getOptions(obj as Record<string, unknown>, id); // Recursively process nested objects
}
return [] return [];
}) });
} }
export function mkOptions<T extends object>(cacheFile: string, object: T, confFile: string = "config.json") { export function mkOptions<T extends object>(cacheFile: string, object: T, confFile: string = "config.json") {
for (const opt of getOptions(object)) for (const opt of getOptions(object as Record<string, unknown>))
opt.init(cacheFile) opt.init(cacheFile)
Utils.ensureDirectory(cacheFile.split("/").slice(0, -1).join("/")) Utils.ensureDirectory(cacheFile.split("/").slice(0, -1).join("/"))
const configFile = `${TMP}/${confFile}` const configFile = `${TMP}/${confFile}`
const values = getOptions(object).reduce((obj, { id, value }) => ({ [id]: value, ...obj }), {}) const values = getOptions(object as Record<string, unknown>).reduce((obj, { id, value }) => ({ [id]: value, ...obj }), {})
Utils.writeFileSync(JSON.stringify(values, null, 2), configFile) Utils.writeFileSync(JSON.stringify(values, null, 2), configFile)
Utils.monitorFile(configFile, () => { Utils.monitorFile(configFile, () => {
const cache = JSON.parse(Utils.readFile(configFile) || "{}") const cache = JSON.parse(Utils.readFile(configFile) || "{}")
for (const opt of getOptions(object)) { for (const opt of getOptions(object as Record<string, unknown>)) {
if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value)) if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value))
opt.value = cache[opt.id] opt.value = cache[opt.id]
} }
@@ -99,7 +102,7 @@ export function mkOptions<T extends object>(cacheFile: string, object: T, confFi
} }
async function reset( async function reset(
[opt, ...list] = getOptions(object), [opt, ...list] = getOptions(object as Record<string, unknown>),
id = opt?.reset(), id = opt?.reset(),
): Promise<Array<string>> { ): Promise<Array<string>> {
if (!opt) if (!opt)
@@ -111,7 +114,7 @@ export function mkOptions<T extends object>(cacheFile: string, object: T, confFi
} }
async function resetTheme( async function resetTheme(
[opt, ...list] = getOptions(object), [opt, ...list] = getOptions(object as Record<string, unknown>),
id = opt?.doResetColor(), id = opt?.doResetColor(),
): Promise<Array<string>> { ): Promise<Array<string>> {
if (!opt) if (!opt)
@@ -124,7 +127,7 @@ export function mkOptions<T extends object>(cacheFile: string, object: T, confFi
return Object.assign(object, { return Object.assign(object, {
configFile, configFile,
array: () => getOptions(object), array: () => getOptions(object as Record<string, unknown>),
async reset() { async reset() {
return (await reset()).join("\n") return (await reset()).join("\n")
}, },
@@ -132,7 +135,7 @@ export function mkOptions<T extends object>(cacheFile: string, object: T, confFi
return (await resetTheme()).join("\n") return (await resetTheme()).join("\n")
}, },
handler(deps: string[], callback: () => void) { handler(deps: string[], callback: () => void) {
for (const opt of getOptions(object)) { for (const opt of getOptions(object as Record<string, unknown>)) {
if (deps.some(i => opt.id.startsWith(i))) if (deps.some(i => opt.id.startsWith(i)))
opt.connect("changed", callback) opt.connect("changed", callback)
} }

View File

@@ -1,3 +1,5 @@
import { layoutMap } from "customModules/kblayout/layouts";
export type KbLabelType = "layout" | "code"; export type KbLabelType = "layout" | "code";
export type KbIcon = "" | "󰌌" | "" | "󰬴" | "󰗊"; export type KbIcon = "" | "󰌌" | "" | "󰬴" | "󰗊";
@@ -26,3 +28,6 @@ export type HyprctlDeviceLayout = {
touch: any[]; touch: any[];
switches: any[]; switches: any[];
}; };
export type LayoutKeys = keyof typeof layoutMap;
export type LayoutValues = typeof layoutMap[LayoutKeys];

View File

@@ -0,0 +1,9 @@
import { Binding } from "lib/utils";
export type InputHandlerEvents = {
onPrimaryClick?: Binding,
onSecondaryClick?: Binding,
onMiddleClick?: Binding,
onScrollUp?: Binding,
onScrollDown?: Binding,
}

10
lib/types/dropdownmenu.d.ts vendored Normal file
View File

@@ -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;

3
lib/types/filechooser.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export type Config = {
[key: string]: string | number | boolean | object;
}

3
lib/types/mpris.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export type LoopStatus = 'none' | 'track' | 'playlist';
export type PlaybackStatus = 'playing' | 'paused' | 'stopped';

View File

@@ -1,3 +1,5 @@
import { WIFI_STATUS_MAP } from "globals/network";
export type AccessPoint = { export type AccessPoint = {
bssid: string | null; bssid: string | null;
address: string | null; address: string | null;
@@ -8,3 +10,5 @@ export type AccessPoint = {
frequency: number; frequency: number;
iconName: string | undefined; iconName: string | undefined;
} }
export type WifiStatus = keyof typeof WIFI_STATUS_MAP;

View File

@@ -1,3 +1,5 @@
import icons from "modules/icons/index";
export interface NotificationArgs { export interface NotificationArgs {
appName?: string; appName?: string;
body?: string; body?: string;
@@ -9,3 +11,5 @@ export interface NotificationArgs {
timeout?: number; timeout?: number;
transient?: boolean; transient?: boolean;
} }
export type NotificationIcon = keyof typeof icons.notifications;

View File

@@ -1,6 +1,10 @@
import { Opt } from "lib/option"; import { Opt } from "lib/option";
import { Variable } from "types/variable"; import { Variable } from "types/variable";
export type RecursiveOptionsObject = {
[key: string]: RecursiveOptionsObject | Opt<string | number | boolean> | Opt<any>;
};
export type Unit = "imperial" | "metric"; export type Unit = "imperial" | "metric";
export type PowerOptions = "sleep" | "reboot" | "logout" | "shutdown"; export type PowerOptions = "sleep" | "reboot" | "logout" | "shutdown";
export type NotificationAnchor = "top" | "top right" | "top left" | "bottom" | "bottom right" | "bottom left" | "left" | "right"; export type NotificationAnchor = "top" | "top right" | "top left" | "bottom" | "bottom right" | "bottom left" | "left" | "right";
@@ -117,3 +121,4 @@ type MatugenVariation =
| "vivid_3" | "vivid_3"
type MatugenTheme = "light" | "dark"; type MatugenTheme = "light" | "dark";

27
lib/types/popupwindow.d.ts vendored Normal file
View File

@@ -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';

8
lib/types/powerprofiles.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import icons from "modules/icons/index";
import PowerProfiles from "types/service/powerprofiles.js"
export type PowerProfiles = InstanceType<typeof PowerProfiles>;
export type PowerProfile = "power-saver" | "balanced" | "performance";
export type PowerProfileObject = {
[key: string]: string;
}

3
lib/types/utils.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import { substitutes } from "lib/icons";
type SubstituteKeys = keyof typeof substitutes;

View File

@@ -1,3 +1,5 @@
import { weatherIcons } from "modules/icons/weather";
export type UnitType = "imperial" | "metric"; export type UnitType = "imperial" | "metric";
export type Weather = { export type Weather = {
@@ -107,3 +109,10 @@ export type Location = {
localtime_epoch: number; localtime_epoch: number;
localtime: string; localtime: string;
} }
export type TemperatureIconColorMap = {
[key: number]: string;
}
export type WeatherIconTitle = keyof typeof weatherIcons;
export type WeatherIcon = typeof weatherIcons[WeatherIconTitle];

View File

@@ -1,3 +1,6 @@
export type Exclusivity = 'normal' | 'ignore' | 'exclusive'; export type Exclusivity = 'normal' | 'ignore' | 'exclusive';
export type Anchor = "left" | "right" | "top" | "down"; export type Anchor = "left" | "right" | "top" | "down";
export type Transition = "none" | "crossfade" | "slide_right" | "slide_left" | "slide_up" | "slide_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';

View File

@@ -8,20 +8,29 @@ import Gdk from "gi://Gdk"
import GLib from "gi://GLib?version=2.0" import GLib from "gi://GLib?version=2.0"
import GdkPixbuf from "gi://GdkPixbuf"; import GdkPixbuf from "gi://GdkPixbuf";
import { NotificationArgs } from "types/utils/notify" import { NotificationArgs } from "types/utils/notify"
import { SubstituteKeys } from "./types/utils";
export type Binding<T> = import("types/service").Binding<any, any, T> export type Binding<T> = import("types/service").Binding<any, any, T>
/** /**
* @returns substitute icon || name || fallback icon * @returns substitute icon || name || fallback icon
*/ */
export function icon(name: string | null, fallback = icons.missing) { export function icon(name: string | null, fallback = icons.missing) {
const validateSubstitute = (name: string): name is SubstituteKeys => name in substitutes;
if (!name) if (!name)
return fallback || "" return fallback || ""
if (GLib.file_test(name, GLib.FileTest.EXISTS)) if (GLib.file_test(name, GLib.FileTest.EXISTS))
return name return name
const icon = (substitutes[name] || name) let icon: string = name;
if (validateSubstitute(name)) {
icon = substitutes[name];
}
if (Utils.lookUpIcon(icon)) if (Utils.lookUpIcon(icon))
return icon return icon

View File

@@ -196,4 +196,4 @@ export default {
patchy_light_snow_with_thunder: "weather-snow-symbolic", patchy_light_snow_with_thunder: "weather-snow-symbolic",
moderate_or_heavy_snow_with_thunder: "weather-snow-symbolic", moderate_or_heavy_snow_with_thunder: "weather-snow-symbolic",
}, },
}; } as const;

View File

@@ -51,4 +51,4 @@ export const weatherIcons = {
moderate_or_heavy_rain_with_thunder: "󰙾", moderate_or_heavy_rain_with_thunder: "󰙾",
patchy_light_snow_with_thunder: "󰼶", patchy_light_snow_with_thunder: "󰼶",
moderate_or_heavy_snow_with_thunder: "󰼶", moderate_or_heavy_snow_with_thunder: "󰼶",
}; } as const;

View File

@@ -1,4 +1,5 @@
const hyprland = await Service.import("hyprland"); const hyprland = await Service.import("hyprland");
import { DropdownMenuProps } from "lib/types/dropdownmenu";
import { Exclusivity } from "lib/types/widget"; import { Exclusivity } from "lib/types/widget";
import { bash } from "lib/utils"; import { bash } from "lib/utils";
import { Monitor } from "types/service/hyprland"; import { Monitor } from "types/service/hyprland";
@@ -99,15 +100,17 @@ setTimeout(() => {
initRender.value = false; initRender.value = false;
}, 2000); }, 2000);
export default ({ export default (
name, {
child, name,
layout = "center", child,
transition, layout = "center",
exclusivity = "ignore" as Exclusivity, transition,
fixed = false, exclusivity = "ignore" as Exclusivity,
...props fixed = false,
}) => ...props
}: DropdownMenuProps
) =>
Widget.Window({ Widget.Window({
name, name,
class_names: [name, "dropdown-menu"], class_names: [name, "dropdown-menu"],

View File

@@ -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"; import { Exclusivity, Transition } from "lib/types/widget";
type Opts = { 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: () => center: () =>
Widget.CenterBox( 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 ({ export default ({
name, name,
child, child,
@@ -157,8 +163,12 @@ export default ({
transition, transition,
exclusivity = "ignore" as Exclusivity, exclusivity = "ignore" as Exclusivity,
...props ...props
}) => }: PopupWindowProps) => {
Widget.Window({ const layoutFn = isValidLayout(layout) ? layout : "center";
const layoutWidget = Layout(name, child, transition)[layoutFn]();
return Widget.Window({
name, name,
class_names: [name, "popup-window"], class_names: [name, "popup-window"],
setup: (w) => w.keybind("Escape", () => App.closeWindow(name)), setup: (w) => w.keybind("Escape", () => App.closeWindow(name)),
@@ -167,6 +177,7 @@ export default ({
exclusivity, exclusivity,
layer: "top", layer: "top",
anchor: ["top", "bottom", "right", "left"], anchor: ["top", "bottom", "right", "left"],
child: Layout(name, child, transition)[layout](), child: layoutWidget,
...props, ...props,
}); });
}

View File

@@ -1,27 +1,31 @@
const getIcon = (audioVol, isMuted) => { const speakerIcons = {
const speakerIcons = {
101: "audio-volume-overamplified-symbolic", 101: "audio-volume-overamplified-symbolic",
66: "audio-volume-high-symbolic", 66: "audio-volume-high-symbolic",
34: "audio-volume-medium-symbolic", 34: "audio-volume-medium-symbolic",
1: "audio-volume-low-symbolic", 1: "audio-volume-low-symbolic",
0: "audio-volume-muted-symbolic", 0: "audio-volume-muted-symbolic",
}; } as const;
const inputIcons = { const inputIcons = {
101: "microphone-sensitivity-high-symbolic",
66: "microphone-sensitivity-high-symbolic", 66: "microphone-sensitivity-high-symbolic",
34: "microphone-sensitivity-medium-symbolic", 34: "microphone-sensitivity-medium-symbolic",
1: "microphone-sensitivity-low-symbolic", 1: "microphone-sensitivity-low-symbolic",
0: "microphone-disabled-symbolic", 0: "microphone-disabled-symbolic",
}; };
const icon = isMuted type IconVolumes = keyof typeof speakerIcons;
? 0
: [101, 66, 34, 1, 0].find((threshold) => threshold <= audioVol * 100);
return { const getIcon = (audioVol: IconVolumes, isMuted: boolean) => {
spkr: speakerIcons[icon], const thresholds: IconVolumes[] = [101, 66, 34, 1, 0];
mic: inputIcons[icon], const icon = isMuted
}; ? 0
: thresholds.find((threshold) => threshold <= audioVol * 100) || 0;
return {
spkr: speakerIcons[icon],
mic: inputIcons[icon],
};
}; };
export { getIcon }; export { getIcon };

View File

@@ -88,7 +88,7 @@ const devices = (bluetooth: Bluetooth, self: Box<Gtk.Widget, unknown>) => {
Widget.Label({ Widget.Label({
vpack: "start", vpack: "start",
class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? "active" : ""} txt-icon`, 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({ Widget.Box({
vertical: true, vertical: true,

View File

@@ -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 { Variable } from "types/variable.js";
import { weatherIcons } from "modules/icons/weather.js"; import { weatherIcons } from "modules/icons/weather.js";
import { isValidWeatherIconTitle } from "globals/weather";
export const HourlyIcon = (theWeather: Variable<Weather>, getNextEpoch: any) => { export const HourlyIcon = (theWeather: Variable<Weather>, getNextEpoch: any) => {
const getIconQuery = (wthr: Weather) => { const getIconQuery = (wthr: Weather): WeatherIconTitle => {
const nextEpoch = getNextEpoch(wthr); const nextEpoch = getNextEpoch(wthr);
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find( const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find(
(h) => h.time_epoch === nextEpoch, (h) => h.time_epoch === nextEpoch,
); );
let iconQuery = weatherAtEpoch?.condition.text if (weatherAtEpoch === undefined) {
return "warning";
}
let iconQuery = weatherAtEpoch.condition.text
.trim() .trim()
.toLowerCase() .toLowerCase()
.replaceAll(" ", "_") .replaceAll(" ", "_");
|| "warning"
;
if (!weatherAtEpoch?.is_day && iconQuery === "partly_cloudy") { if (!weatherAtEpoch?.is_day && iconQuery === "partly_cloudy") {
iconQuery = "partly_cloudy_night"; iconQuery = "partly_cloudy_night";
} }
return iconQuery;
if (isValidWeatherIconTitle(iconQuery)) {
return iconQuery;
} else {
return "warning";
}
} }
return Widget.Box({ return Widget.Box({
hpack: "center", hpack: "center",
child: theWeather.bind("value").as((w) => { child: theWeather.bind("value").as((w) => {
let weatherIcn = "-"; let weatherIcn: WeatherIcon;
const iconQuery = getIconQuery(w); const iconQuery = getIconQuery(w);
weatherIcn = weatherIcons[iconQuery]; weatherIcn = weatherIcons[iconQuery] || weatherIcons["warning"];
return Widget.Label({ return Widget.Label({
hpack: "center", hpack: "center",
class_name: "hourly-weather-icon txt-icon", class_name: "hourly-weather-icon txt-icon",

View File

@@ -6,7 +6,7 @@ const { terminal } = options;
const { enable_gpu } = options.menus.dashboard.stats; const { enable_gpu } = options.menus.dashboard.stats;
const Stats = () => { const Stats = () => {
const divide = ([total, free]) => free / total; const divide = ([total, free]: number[]) => free / total;
const formatSizeInGB = (sizeInKB: number) => const formatSizeInGB = (sizeInKB: number) =>
Number((sizeInKB / 1024 ** 2).toFixed(2)); Number((sizeInKB / 1024 ** 2).toFixed(2));
@@ -26,7 +26,8 @@ const Stats = () => {
return 0; return 0;
} }
return divide([100, cpuOut.split(/\s+/)[1].replace(",", ".")]); const freeCpu = parseFloat(cpuOut.split(/\s+/)[1].replace(",", "."));
return divide([100, freeCpu]);
}, },
], ],
}); });

View File

@@ -1,7 +1,11 @@
const powerProfiles = await Service.import("powerprofiles"); const powerProfiles = await Service.import("powerprofiles");
import { PowerProfile, PowerProfileObject, PowerProfiles } from "lib/types/powerprofiles.js";
import icons from "../../../icons/index.js"; import icons from "../../../icons/index.js";
const EnergyProfiles = () => { const EnergyProfiles = () => {
const isValidProfile = (profile: string): profile is PowerProfile =>
profile === "power-saver" || profile === "balanced" || profile === "performance";
return Widget.Box({ return Widget.Box({
class_name: "menu-section-container energy", class_name: "menu-section-container energy",
vertical: true, vertical: true,
@@ -21,13 +25,20 @@ const EnergyProfiles = () => {
vpack: "fill", vpack: "fill",
vexpand: true, vexpand: true,
vertical: true, vertical: true,
children: powerProfiles.bind("profiles").as((profiles) => { children: powerProfiles.bind("profiles").as((profiles: PowerProfiles) => {
return profiles.map((prof) => { return profiles.map((prof: PowerProfileObject) => {
const ProfileLabels = { const profileLabels = {
"power-saver": "Power Saver", "power-saver": "Power Saver",
balanced: "Balanced", balanced: "Balanced",
performance: "Performance", performance: "Performance",
}; };
const profileType = prof.Profile;
if (!isValidProfile(profileType)) {
return profileLabels.balanced;
}
return Widget.Button({ return Widget.Button({
on_primary_click: () => { on_primary_click: () => {
powerProfiles.active_profile = prof.Profile; powerProfiles.active_profile = prof.Profile;
@@ -39,11 +50,11 @@ const EnergyProfiles = () => {
children: [ children: [
Widget.Icon({ Widget.Icon({
class_name: "power-profile-icon", class_name: "power-profile-icon",
icon: icons.powerprofile[prof.Profile], icon: icons.powerprofile[profileType],
}), }),
Widget.Label({ Widget.Label({
class_name: "power-profile-label", class_name: "power-profile-label",
label: ProfileLabels[prof.Profile], label: profileLabels[profileType],
}), }),
], ],
}), }),

View File

@@ -1,17 +1,24 @@
import { MprisPlayer } from "types/service/mpris.js"; import { MprisPlayer } from "types/service/mpris.js";
import icons from "../../../icons/index.js"; import icons from "../../../icons/index.js";
import { LoopStatus, PlaybackStatus } from "lib/types/mpris.js";
const media = await Service.import("mpris"); const media = await Service.import("mpris");
const Controls = (getPlayerInfo: Function) => { 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) => { const isLoopActive = (player: MprisPlayer) => {
return player["loop-status"] !== null && return player["loop_status"] !== null &&
["track", "playlist"].includes(player["loop-status"].toLowerCase()) ["track", "playlist"].includes(player["loop_status"].toLowerCase())
? "active" ? "active"
: ""; : "";
}; };
const isShuffleActive = (player: MprisPlayer) => { const isShuffleActive = (player: MprisPlayer) => {
return player["shuffle-status"] !== null && player["shuffle-status"] return player["shuffle_status"] !== null && player["shuffle_status"]
? "active" ? "active"
: ""; : "";
}; };
@@ -98,13 +105,18 @@ const Controls = (getPlayerInfo: Function) => {
media, media,
"changed", "changed",
() => { () => {
const foundPlayer = getPlayerInfo(); const foundPlayer: MprisPlayer = getPlayerInfo();
if (foundPlayer === undefined) { if (foundPlayer === undefined) {
return icons.mpris["paused"]; return icons.mpris["paused"];
} }
return icons.mpris[ const playbackStatus = foundPlayer.play_back_status?.toLowerCase();
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({ child: Widget.Icon({
setup: (self) => { setup: (self) => {
self.hook(media, () => { self.hook(media, () => {
const foundPlayer = getPlayerInfo(); const foundPlayer: MprisPlayer = getPlayerInfo();
if (foundPlayer === undefined) { if (foundPlayer === undefined) {
self.icon = icons.mpris.loop["none"]; self.icon = icons.mpris.loop["none"];
return; return;
} }
self.icon = const loopStatus = foundPlayer.loop_status?.toLowerCase();
foundPlayer.loop_status === null
? icons.mpris.loop["none"] if (loopStatus && isValidLoopStatus(loopStatus)) {
: icons.mpris.loop[ self.icon = icons.mpris.loop[loopStatus];
foundPlayer.loop_status?.toLowerCase() }
]; else {
self.icon = icons.mpris.loop["none"];
}
}); });
}, },
}), }),

View File

@@ -37,17 +37,17 @@ const Media = () => {
}; };
const isPlaying = media.players.find( const isPlaying = media.players.find(
(p) => p["play-back-status"] === "Playing", (p) => p["play_back_status"] === "Playing",
); );
const playerStillExists = media.players.some( const playerStillExists = media.players.some(
(p) => curPlayer.value === p["bus-name"], (p) => curPlayer.value === p["bus_name"],
); );
const nextPlayerUp = media.players.sort( const nextPlayerUp = media.players.sort(
(a, b) => (a, b) =>
statusOrder[a["play-back-status"]] - statusOrder[a["play_back_status"]] -
statusOrder[b["play-back-status"]], statusOrder[b["play_back_status"]],
)[0].bus_name; )[0].bus_name;
if (isPlaying || !playerStillExists) { if (isPlaying || !playerStillExists) {

View File

@@ -1,7 +1,8 @@
import { Network } from "types/service/network.js"; 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 { Variable } from "types/variable.js";
import { getWifiIcon } from "../utils.js"; import { getWifiIcon } from "../utils.js";
import { WIFI_STATUS_MAP } from "globals/network.js";
const renderWAPs = (self: any, network: Network, staging: Variable<AccessPoint>, connecting: Variable<string>) => { const renderWAPs = (self: any, network: Network, staging: Variable<AccessPoint>, connecting: Variable<string>) => {
const getIdBySsid = (ssid: string, nmcliOutput: string) => { const getIdBySsid = (ssid: string, nmcliOutput: string) => {
const lines = nmcliOutput.trim().split("\n"); const lines = nmcliOutput.trim().split("\n");
@@ -14,33 +15,36 @@ const renderWAPs = (self: any, network: Network, staging: Variable<AccessPoint>,
return null; return null;
}; };
const WifiStatusMap = { const isValidWifiStatus = (status: string): status is WifiStatus => {
unknown: "Status Unknown", return status in WIFI_STATUS_MAP;
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 getWifiStatus = () => {
const wifiState = network.wifi.state?.toLowerCase();
if (wifiState && isValidWifiStatus(wifiState)) {
return WIFI_STATUS_MAP[wifiState];
}
return WIFI_STATUS_MAP["unknown"];
}
self.hook(network, () => { self.hook(network, () => {
Utils.merge([staging.bind("value"), connecting.bind("value")], () => { 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 // trying to access the "access_points" property. So we must validate that
// it's not 'undefined' // it's not 'undefined'
// // --
// Also this is an AGS bug that needs to be fixed // 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 dedupeWAPs = () => {
const dedupMap = {}; const dedupMap: Record<string, AccessPoint> = {};
WAPs.forEach((item: AccessPoint) => { WAPs.forEach((item: AccessPoint) => {
if (item.ssid !== null && !Object.hasOwnProperty.call(dedupMap, item.ssid)) { if (item.ssid !== null && !Object.hasOwnProperty.call(dedupMap, item.ssid)) {
dedupMap[item.ssid] = item; dedupMap[item.ssid] = item;
@@ -153,8 +157,7 @@ const renderWAPs = (self: any, network: Network, staging: Variable<AccessPoint>,
child: Widget.Label({ child: Widget.Label({
hpack: "start", hpack: "start",
class_name: "connection-status dim", class_name: "connection-status dim",
label: label: getWifiStatus()
WifiStatusMap[network.wifi.state.toLowerCase()],
}), }),
}), }),
], ],

View File

@@ -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 }) => { const NotificationIcon = ({ app_entry = "", app_icon = "", app_name = "" }: Partial<Notification>) => {
let icon = icons.fallback.notification; return Widget.Box({
css: `
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: `
min-width: 2rem; min-width: 2rem;
min-height: 2rem; min-height: 2rem;
`, `,
child: Widget.Icon({ child: Widget.Icon({
class_name: "notification-icon menu", class_name: "notification-icon menu",
icon, icon: getNotificationIcon(app_name, app_icon, app_entry),
}), }),
}); });
}; };
export { NotificationIcon }; export { NotificationIcon };

View File

@@ -10,7 +10,7 @@ export const NotificationPager = (curPage: Variable<number>) => {
class_name: "notification-menu-pager", class_name: "notification-menu-pager",
hexpand: true, hexpand: true,
vexpand: false, 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 [ return [
Widget.Button({ Widget.Button({
hexpand: true, hexpand: true,

View File

@@ -1,4 +1,3 @@
import icons from "lib/icons";
import { PowerOptions } from "lib/types/options"; import { PowerOptions } from "lib/types/options";
import options from "options"; import options from "options";
import powermenu from "../power/helpers/actions"; import powermenu from "../power/helpers/actions";

View File

@@ -1,9 +1,6 @@
import options from "options.js";
import DropdownMenu from "../DropdownMenu.js"; import DropdownMenu from "../DropdownMenu.js";
import { PowerButton } from "./button.js"; import { PowerButton } from "./button.js";
const { showLabel } = options.menus.power;
export default () => { export default () => {
return DropdownMenu({ return DropdownMenu({
name: "powerdropdownmenu", name: "powerdropdownmenu",

View File

@@ -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 }) => { const NotificationIcon = ({ app_entry = "", app_icon = "", app_name = "" }: Partial<Notification>) => {
let icon = icons.fallback.notification; return Widget.Box({
css: `
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: `
min-width: 2rem; min-width: 2rem;
min-height: 2rem; min-height: 2rem;
`, `,
child: Widget.Icon({ child: Widget.Icon({
class_name: "notification-icon", class_name: "notification-icon",
icon, icon: getNotificationIcon(app_name, app_icon, app_entry)
}), }),
}); });
}; };
export { NotificationIcon }; export { NotificationIcon };

View File

@@ -1,8 +1,7 @@
import { OSDOrientation } from "lib/types/options";
import brightness from "services/Brightness" import brightness from "services/Brightness"
const audio = await Service.import("audio") const audio = await Service.import("audio")
export const OSDIcon = (ort: OSDOrientation) => { export const OSDIcon = () => {
return Widget.Box({ return Widget.Box({
class_name: "osd-icon-container", class_name: "osd-icon-container",
hexpand: true, hexpand: true,

View File

@@ -9,6 +9,7 @@ const audio = await Service.import("audio")
const { const {
enable, enable,
duration,
orientation, orientation,
location, location,
active_monitor, active_monitor,
@@ -21,8 +22,6 @@ hyprland.active.connect("changed", () => {
curMonitor.value = hyprland.active.monitor.id; curMonitor.value = hyprland.active.monitor.id;
}) })
const DELAY = 2500;
let count = 0 let count = 0
const handleReveal = (self: any, property: string) => { const handleReveal = (self: any, property: string) => {
if (!enable.value) { if (!enable.value) {
@@ -30,7 +29,7 @@ const handleReveal = (self: any, property: string) => {
} }
self[property] = true self[property] = true
count++ count++
Utils.timeout(DELAY, () => { Utils.timeout(duration.value, () => {
count-- count--
if (count === 0) if (count === 0)
@@ -39,8 +38,6 @@ const handleReveal = (self: any, property: string) => {
} }
const renderOSD = () => { const renderOSD = () => {
return Widget.Revealer({ return Widget.Revealer({
transition: "crossfade", transition: "crossfade",
reveal_child: false, reveal_child: false,
@@ -72,16 +69,16 @@ const renderOSD = () => {
children: orientation.bind("value").as(ort => { children: orientation.bind("value").as(ort => {
if (ort === "vertical") { if (ort === "vertical") {
return [ return [
OSDLabel(ort), OSDLabel(),
OSDBar(ort), OSDBar(ort),
OSDIcon(ort) OSDIcon()
] ]
} }
return [ return [
OSDIcon(ort), OSDIcon(),
OSDBar(ort), OSDBar(ort),
OSDLabel(ort), OSDLabel(),
] ]
}) })
}) })

View File

@@ -1,9 +1,8 @@
import { OSDOrientation } from "lib/types/options";
import brightness from "services/Brightness" import brightness from "services/Brightness"
import options from "options" import options from "options"
const audio = await Service.import("audio") const audio = await Service.import("audio")
export const OSDLabel = (ort: OSDOrientation) => { export const OSDLabel = () => {
return Widget.Box({ return Widget.Box({
class_name: "osd-label-container", class_name: "osd-label-container",
hexpand: true, hexpand: true,

View File

@@ -109,6 +109,7 @@ const options = mkOptions(OPTIONS, {
}, },
osd: { osd: {
scaling: opt(100), scaling: opt(100),
duration: opt(2500),
enable: opt(true), enable: opt(true),
orientation: opt<OSDOrientation>("vertical"), orientation: opt<OSDOrientation>("vertical"),
opacity: opt(100), opacity: opt(100),

View File

@@ -1,8 +1,10 @@
import options from "options"; import options from "options";
import { bash, dependencies } from "lib/utils"; 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 { initializeTrackers } from "./options_trackers";
import { generateMatugenColors, replaceHexValues } from "../services/matugen/index"; import { generateMatugenColors, replaceHexValues } from "../services/matugen/index";
import { isHexColor, isOpt, isRecursiveOptionsObject } from "globals/variables";
import { Opt } from "lib/option";
const deps = [ const deps = [
"font", "font",
@@ -13,27 +15,37 @@ const deps = [
"bar.battery.blocks", "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[]; let result = [] as string[];
for (let key in theme) { for (let key in theme) {
if (theme.hasOwnProperty(key)) { if (!theme.hasOwnProperty(key)) {
const value = theme[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 newPrefix = prefix ? `${prefix}-${key}` : key;
const replacedValue = isColor && matugenColors !== undefined ? replaceHexValues(value.value, matugenColors) : value.value;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) { const isColor = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value.value);
if (typeof value.value !== 'undefined') { const replacedValue = isColor && matugenColors !== undefined ? replaceHexValues(value.value, matugenColors) : value.value;
result.push(`$${newPrefix}: ${replacedValue};`);
} else { if (typeof value === 'function') {
result = result.concat(extractVariables(value, newPrefix, matugenColors)); result.push(`$${newPrefix}: ${replacedValue};`);
} continue;
} else if (typeof value === 'function' && value.name === 'opt') { }
result.push(`$${newPrefix}: ${replacedValue};`); 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; return result;
} }

View File

@@ -31,8 +31,8 @@
border-radius: if($osd-orientation =="vertical", 0em 0em $osd-radius $osd-radius, $osd-radius 0em 0em $osd-radius ); border-radius: if($osd-orientation =="vertical", 0em 0em $osd-radius $osd-radius, $osd-radius 0em 0em $osd-radius );
.osd-icon { .osd-icon {
font-size: 2em; font-size: 2.1em;
padding: if($osd-orientation =="vertical", 0.2em 0em, 0em 0.2em); padding: if($osd-orientation =="vertical", 0.2em 0em, 0em 0.4em);
color: $osd-icon; color: $osd-icon;
} }
} }

View File

@@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"noEmit": true,
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "ES2022",
@@ -10,11 +11,14 @@
"checkJs": true, "checkJs": true,
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"strictNullChecks": true,
"alwaysStrict": true,
"noImplicitThis": true,
"baseUrl": ".", "baseUrl": ".",
"typeRoots": [ "typeRoots": [
"types", "types",
"lib/types/globals", "lib/types/globals",
], ],
"skipLibCheck": true "skipLibCheck": true,
} }
} }

View File

@@ -12,6 +12,7 @@ export const OSDSettings = () => {
children: [ children: [
Header('On Screen Display'), Header('On Screen Display'),
Option({ opt: options.theme.osd.enable, title: 'Enabled', type: 'boolean' }), 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.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.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' }), Option({ opt: options.theme.osd.monitor, title: 'Monitor', subtitle: 'The ID of the monitor on which to display the OSD', type: 'number' }),

View File

@@ -2,14 +2,15 @@ import Gtk from "gi://Gtk?version=3.0";
import Gio from "gi://Gio" import Gio from "gi://Gio"
import { bash, Notify } from "lib/utils"; import { bash, Notify } from "lib/utils";
import icons from "lib/icons" import icons from "lib/icons"
import { Config } from "lib/types/filechooser";
import { hexColorPattern } from "globals/useTheme";
import { isHexColor } from "globals/variables";
const whiteListedThemeProp = [ const whiteListedThemeProp = [
"theme.bar.buttons.style" "theme.bar.buttons.style"
]; ];
export const loadJsonFile = (filePath: string): Config | null => {
// Helper functions
export const loadJsonFile = (filePath: string): object | null => {
let file = Gio.File.new_for_path(filePath as string); let file = Gio.File.new_for_path(filePath as string);
let [success, content] = file.load_contents(null); let [success, content] = file.load_contents(null);
@@ -32,10 +33,8 @@ export const saveConfigToFile = (config: object, filePath: string): void => {
dataOutputStream.close(null); dataOutputStream.close(null);
} }
export const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; export const filterConfigForThemeOnly = (config: Config): Config => {
let filteredConfig: Config = {};
export const filterConfigForThemeOnly = (config: object) => {
let filteredConfig = {};
for (let key in config) { for (let key in config) {
if (typeof config[key] === 'string' && hexColorPattern.test(config[key])) { if (typeof config[key] === 'string' && hexColorPattern.test(config[key])) {
@@ -47,8 +46,8 @@ export const filterConfigForThemeOnly = (config: object) => {
return filteredConfig; return filteredConfig;
}; };
export const filterConfigForNonTheme = (config: object) => { export const filterConfigForNonTheme = (config: Config): Config => {
let filteredConfig = {}; let filteredConfig: Config = {};
for (let key in config) { for (let key in config) {
if (whiteListedThemeProp.includes(key)) { if (whiteListedThemeProp.includes(key)) {
continue; continue;
@@ -75,12 +74,11 @@ export const saveFileDialog = (filePath: string, themeOnly: boolean): void => {
let jsonObject = JSON.parse(jsonString); let jsonObject = JSON.parse(jsonString);
// Function to filter hex color pairs // Function to filter hex color pairs
const filterHexColorPairs = (jsonObject: object) => { const filterHexColorPairs = (jsonObject: Config) => {
const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; let filteredObject: Config = {};
let filteredObject = {};
for (let key in jsonObject) { 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]; filteredObject[key] = jsonObject[key];
} else if (whiteListedThemeProp.includes(key)) { } else if (whiteListedThemeProp.includes(key)) {
filteredObject[key] = jsonObject[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) // Function to filter out hex color pairs (keep only non-hex color value)
const filterOutHexColorPairs = (jsonObject: object) => { const filterOutHexColorPairs = (jsonObject: Config) => {
const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; let filteredObject: Config = {};
let filteredObject = {};
for (let key in jsonObject) { for (let key in jsonObject) {
// do not add key-value pair if its in whiteListedThemeProp // do not add key-value pair if its in whiteListedThemeProp
@@ -102,7 +99,7 @@ export const saveFileDialog = (filePath: string, themeOnly: boolean): void => {
continue; continue;
} }
if (!(typeof jsonObject[key] === 'string' && hexColorPattern.test(jsonObject[key]))) { if (!(typeof jsonObject[key] === 'string' && isHexColor(jsonObject[key]))) {
filteredObject[key] = jsonObject[key]; filteredObject[key] = jsonObject[key];
} }
} }
@@ -192,8 +189,19 @@ export const importFiles = (themeOnly: boolean = false): void => {
return; return;
} }
if (response === Gtk.ResponseType.ACCEPT) { if (response === Gtk.ResponseType.ACCEPT) {
let filePath = dialog.get_filename(); let filePath: string | null = dialog.get_filename();
let importedConfig = loadJsonFile(filePath as string);
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) { if (!importedConfig) {
dialog.destroy(); dialog.destroy();