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

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

View File

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

View File

@@ -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"],

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";
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,
});
}

View File

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

View File

@@ -88,7 +88,7 @@ const devices = (bluetooth: Bluetooth, self: Box<Gtk.Widget, unknown>) => {
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,

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 { weatherIcons } from "modules/icons/weather.js";
import { isValidWeatherIconTitle } from "globals/weather";
export const HourlyIcon = (theWeather: Variable<Weather>, 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",

View File

@@ -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]);
},
],
});

View File

@@ -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],
}),
],
}),

View File

@@ -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"];
}
});
},
}),

View File

@@ -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) {

View File

@@ -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<AccessPoint>, connecting: Variable<string>) => {
const getIdBySsid = (ssid: string, nmcliOutput: string) => {
const lines = nmcliOutput.trim().split("\n");
@@ -14,33 +15,36 @@ const renderWAPs = (self: any, network: Network, staging: Variable<AccessPoint>,
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<string, AccessPoint> = {};
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<AccessPoint>,
child: Widget.Label({
hpack: "start",
class_name: "connection-status dim",
label:
WifiStatusMap[network.wifi.state.toLowerCase()],
label: getWifiStatus()
}),
}),
],

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 }) => {
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<Notification>) => {
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 };

View File

@@ -10,7 +10,7 @@ export const NotificationPager = (curPage: Variable<number>) => {
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,

View File

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

View File

@@ -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",

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 }) => {
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<Notification>) => {
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 };

View File

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

View File

@@ -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(),
]
})
})

View File

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