Upgrade to Agsv2 + Astal (#533)

* migrate to astal

* Reorganize project structure.

* progress

* Migrate Dashboard and Window Title modules.

* Migrate clock and notification bar modules.

* Remove unused code

* Media menu

* Rework network and volume modules

* Finish custom modules.

* Migrate battery bar module.

* Update battery module and organize helpers.

* Migrate workspace module.

* Wrap up bar modules.

* Checkpoint before I inevitbly blow something up.

* Updates

* Fix event propagation logic.

* Type fixes

* More type fixes

* Fix padding for event boxes.

* Migrate volume menu and refactor scroll event handlers.

* network module WIP

* Migrate network service.

* Migrate bluetooth menu

* Updates

* Migrate notifications

* Update scrolling behavior for custom modules.

* Improve popup notifications and add timer functionality.

* Migration notifications menu header/controls.

* Migrate notifications menu and consolidate notifications menu code.

* Migrate power menu.

* Dashboard progress

* Migrate dashboard

* Migrate media menu.

* Reduce media menu nesting.

* Finish updating media menu bindings to navigate active player.

* Migrate battery menu

* Consolidate code

* Migrate calendar menu

* Fix workspace logic to update on client add/change/remove and consolidate code.

* Migrate osd

* Consolidate hyprland service connections.

* Implement startup dropdown menu position allocation.

* Migrate settings menu (WIP)

* Settings dialo menu fixes

* Finish Dashboard menu

* Type updates

* update submoldule for types

* update github ci

* ci

* Submodule update

* Ci updates

* Remove type checking for now.

* ci fix

* Fix a bunch of stuff, losing track... need rest. Brb coffee

* Validate dropdown menu before render.

* Consolidate code and add auto-hide functionality.

* Improve auto-hide behavior.

* Consolidate audio menu code

* Organize bluetooth code

* Improve active player logic

* Properly dismiss a notification on action button resolution.

* Implement CLI command engine and migrate CLI commands.

* Handle variable disposal

* Bar component fixes and add hyprland startup rules.

* Handle potentially null bindings network and bluetooth bindings.

* Handle potentially null wired adapter.

* Fix GPU stats

* Handle poller for GPU

* Fix gpu bar logic.

* Clean up logic for stat bars.

* Handle wifi and wired bar icon bindings.

* Fix battery percentages

* Fix switch behavior

* Wifi staging fixes

* Reduce redundant hyprland service calls.

* Code cleanup

* Document the option code and reduce redundant calls to optimize performance.

* Remove outdated comment.

* Add JSDocs

* Add meson to build hyprpanel

* Consistency updates

* Organize commands

* Fix images not showing up on notifications.

* Remove todo

* Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands.

* Handle SRC directory for the bundled/built hyprpanel.

* Add namespaces to all windows

* Migrate systray

* systray updates

* Update meson to include ts, tsx and scss files.

* Remove log from meson

* Fix file choose path and make it float.

* Added a command to check the dependency status

* Update dep names.

* Get scale directly from env

* Add todo
This commit is contained in:
Jas Singh
2024-12-20 18:10:10 -08:00
committed by GitHub
parent 955eed6c60
commit 2ffd602910
605 changed files with 19543 additions and 15999 deletions

View File

@@ -0,0 +1,50 @@
import { bind, Variable } from 'astal';
import { hyprlandService } from '../constants/services';
import { App } from 'astal/gtk3';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
import { forceUpdater } from 'src/components/bar/modules/workspaces/helpers';
import options from 'src/options';
const { autoHide } = options.bar;
const focusedClient = (focusedClient: AstalHyprland.Client): void => {
const fullscreenBinding = bind(focusedClient, 'fullscreen');
if (!focusedClient) {
return;
}
Variable.derive([bind(fullscreenBinding)], (isFullScreen) => {
if (autoHide.get() === 'fullscreen') {
App.get_window(`bar-${focusedClient.monitor.id}`)?.set_visible(!isFullScreen);
}
});
};
export const initializeAutoHide = (): void => {
Variable.derive([bind(autoHide), bind(forceUpdater), bind(hyprlandService, 'workspaces')], (shouldAutohide) => {
if (shouldAutohide === 'never') {
hyprlandService.monitors.forEach((monitor) => {
App.get_window(`bar-${monitor.id}`)?.set_visible(true);
});
}
hyprlandService.workspaces.map((workspace) => {
if (autoHide.get() === 'single-window') {
App.get_window(`bar-${workspace.monitor.id}`)?.set_visible(workspace.clients.length !== 1);
}
});
});
Variable.derive([bind(hyprlandService, 'focusedClient')], (currentClient) => {
focusedClient(currentClient);
});
Variable.derive([bind(autoHide)], (shouldAutohide) => {
if (shouldAutohide === 'fullscreen') {
hyprlandService.workspaces.forEach((workspace) => {
App.get_window(`bar-${workspace.monitor.id}`)?.set_visible(!workspace.hasFullscreen);
});
}
});
};

View File

@@ -0,0 +1,28 @@
import { batteryService } from '../constants/services';
import icons from '../icons/icons';
import { Notify } from '../utils';
export function warnOnLowBattery(): void {
batteryService.connect('notify::percent', () => {
const { lowBatteryThreshold, lowBatteryNotification, lowBatteryNotificationText, lowBatteryNotificationTitle } =
options.menus.power;
if (!lowBatteryNotification.get() || batteryService.charging) {
return;
}
const lowThreshold = lowBatteryThreshold.get();
if (batteryService.percentage === lowThreshold || batteryService.percentage === lowThreshold / 2) {
Notify({
summary: lowBatteryNotificationTitle
.get()
.replace('/$POWER_LEVEL/g', batteryService.percentage.toString()),
body: lowBatteryNotificationText.get().replace('/$POWER_LEVEL/g', batteryService.percentage.toString()),
iconName: icons.ui.warning,
urgency: 'critical',
timeout: 7000,
});
}
});
}

View File

@@ -0,0 +1,23 @@
import { execAsync } from 'astal';
import { hyprlandService } from '../constants/services';
const floatSettingsDialog = (): void => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^(hyprpanel-settings)$"']);
hyprlandService.connect('config-reloaded', () => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^(hyprpanel-settings)$"']);
});
};
const floatFilePicker = (): void => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^((Save|Import) Hyprpanel.*)$"']);
hyprlandService.connect('config-reloaded', () => {
execAsync(['bash', '-c', 'hyprctl keyword windowrulev2 "float, title:^((Save|Import) Hyprpanel.*)$"']);
});
};
export const hyprlandSettings = (): void => {
floatSettingsDialog();
floatFilePicker();
};

View File

@@ -0,0 +1,10 @@
import './autoHide';
import { initializeAutoHide } from './autoHide';
import { warnOnLowBattery } from './batteryWarning';
import { hyprlandSettings } from './hyprlandRules';
export const initializeSystemBehaviors = (): void => {
warnOnLowBattery();
initializeAutoHide();
hyprlandSettings();
};

142
src/lib/constants/colors.ts Normal file
View File

@@ -0,0 +1,142 @@
export const namedColors = new Set([
'alice blue',
'antique white',
'aqua',
'aquamarine',
'azure',
'beige',
'bisque',
'black',
'blanched almond',
'blue',
'blue violet',
'brown',
'burlywood',
'cadet blue',
'chartreuse',
'chocolate',
'coral',
'cornflower blue',
'cornsilk',
'crimson',
'cyan',
'dark blue',
'dark cyan',
'dark goldenrod',
'dark gray',
'dark green',
'dark khaki',
'dark magenta',
'dark olive green',
'dark orange',
'dark orchid',
'dark red',
'dark salmon',
'dark sea green',
'dark slate blue',
'dark slate gray',
'dark turquoise',
'dark violet',
'deep pink',
'deep sky blue',
'dim gray',
'dodger blue',
'firebrick',
'floral white',
'forest green',
'fuchsia',
'gainsboro',
'ghost white',
'gold',
'goldenrod',
'gray',
'green',
'green yellow',
'honeydew',
'hot pink',
'indian red',
'indigo',
'ivory',
'khaki',
'lavender',
'lavender blush',
'lawn green',
'lemon chiffon',
'light blue',
'light coral',
'light cyan',
'light goldenrod yellow',
'light green',
'light grey',
'light pink',
'light salmon',
'light sea green',
'light sky blue',
'light slate gray',
'light steel blue',
'light yellow',
'lime',
'lime green',
'linen',
'magenta',
'maroon',
'medium aquamarine',
'medium blue',
'medium orchid',
'medium purple',
'medium sea green',
'medium slate blue',
'medium spring green',
'medium turquoise',
'medium violet red',
'midnight blue',
'mint cream',
'misty rose',
'moccasin',
'navajo white',
'navy',
'old lace',
'olive',
'olive drab',
'orange',
'orange red',
'orchid',
'pale goldenrod',
'pale green',
'pale turquoise',
'pale violet red',
'papaya whip',
'peach puff',
'peru',
'pink',
'plum',
'powder blue',
'purple',
'red',
'rosy brown',
'royal blue',
'saddle brown',
'salmon',
'sandy brown',
'sea green',
'seashell',
'sienna',
'silver',
'sky blue',
'slate blue',
'slate gray',
'snow',
'spring green',
'steel blue',
'tan',
'teal',
'thistle',
'tomato',
'turquoise',
'violet',
'wheat',
'white',
'white smoke',
'yellow',
'yellow green',
]);

View File

@@ -0,0 +1,31 @@
export const distroIcons = [
['deepin', ''],
['fedora', ''],
['arch', ''],
['nixos', ''],
['debian', ''],
['opensuse-tumbleweed', ''],
['ubuntu', ''],
['endeavouros', ''],
['manjaro', ''],
['popos', ''],
['garuda', ''],
['zorin', ''],
['mxlinux', ''],
['arcolinux', ''],
['gentoo', ''],
['artix', ''],
['centos', ''],
['hyperbola', ''],
['kubuntu', ''],
['mandriva', ''],
['xerolinux', ''],
['parabola', ''],
['void', ''],
['linuxmint', ''],
['archlabs', ''],
['devuan', ''],
['freebsd', ''],
['openbsd', ''],
['slackware', ''],
];

View File

@@ -0,0 +1,22 @@
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
type DeviceSate = AstalNetwork.DeviceState;
type DevceStates = {
[key in DeviceSate]: string;
};
export const DEVICE_STATES: DevceStates = {
[AstalNetwork.DeviceState.UNKNOWN]: 'Unknown',
[AstalNetwork.DeviceState.UNMANAGED]: 'Unmanaged',
[AstalNetwork.DeviceState.UNAVAILABLE]: 'Unavailable',
[AstalNetwork.DeviceState.DISCONNECTED]: 'Disconnected',
[AstalNetwork.DeviceState.PREPARE]: 'Prepare',
[AstalNetwork.DeviceState.CONFIG]: 'Config',
[AstalNetwork.DeviceState.NEED_AUTH]: 'Need Authentication',
[AstalNetwork.DeviceState.IP_CONFIG]: 'IP Configuration',
[AstalNetwork.DeviceState.IP_CHECK]: 'IP Check',
[AstalNetwork.DeviceState.SECONDARIES]: 'Secondaries',
[AstalNetwork.DeviceState.ACTIVATED]: 'Activated',
[AstalNetwork.DeviceState.DEACTIVATING]: 'Deactivating',
[AstalNetwork.DeviceState.FAILED]: 'Failed',
};

View File

@@ -0,0 +1,37 @@
import { Gtk } from 'astal/gtk3';
import { DropdownMenuList } from '../types/options';
export const StackTransitionMap = {
none: Gtk.StackTransitionType.NONE,
crossfade: Gtk.StackTransitionType.CROSSFADE,
slide_right: Gtk.StackTransitionType.SLIDE_RIGHT,
slide_left: Gtk.StackTransitionType.SLIDE_LEFT,
slide_up: Gtk.StackTransitionType.SLIDE_UP,
slide_down: Gtk.StackTransitionType.SLIDE_DOWN,
};
export const RevealerTransitionMap = {
none: Gtk.RevealerTransitionType.NONE,
crossfade: Gtk.RevealerTransitionType.CROSSFADE,
slide_right: Gtk.RevealerTransitionType.SLIDE_RIGHT,
slide_left: Gtk.RevealerTransitionType.SLIDE_LEFT,
slide_up: Gtk.RevealerTransitionType.SLIDE_UP,
slide_down: Gtk.RevealerTransitionType.SLIDE_DOWN,
};
export const dropdownMenuList = [
'dashboardmenu',
'audiomenu',
'mediamenu',
'networkmenu',
'bluetoothmenu',
'notificationsmenu',
'calendarmenu',
'energymenu',
'powerdropdownmenu',
'settings-dialog',
] as const;
export const isDropdownMenu = (name: string): name is DropdownMenuList => {
return dropdownMenuList.includes(name as DropdownMenuList);
};

View File

@@ -0,0 +1,34 @@
/**
* NOTE: This approach is not recommended if the program is going to be
* running as a client.
* ---------------------------------------
* Hyprpanel will not be, so this is fine.
* ---------------------------------------
*/
import Hyprland from 'gi://AstalHyprland';
export const hyprlandService = Hyprland.get_default();
import AstalMpris from 'gi://AstalMpris?version=0.1';
export const mprisService = AstalMpris.get_default();
import AstalWp from 'gi://AstalWp?version=0.1';
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
export const audioService = wireplumber.audio;
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
export const networkService = AstalNetwork.get_default();
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
export const bluetoothService = AstalBluetooth.get_default();
import AstalBattery from 'gi://AstalBattery?version=0.1';
export const batteryService = AstalBattery.get_default();
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
export const notifdService = AstalNotifd.get_default();
import Brightness from 'src/services/Brightness';
export const brightnessService = Brightness.get_default();
import AstalPowerProfiles from 'gi://AstalPowerProfiles?version=0.1';
export const powerProfilesService = AstalPowerProfiles.get_default();

View File

@@ -0,0 +1,29 @@
export const defaultApplicationIcons = {
'[dD]iscord': '󰙯',
'^thunderbird': '',
'class:wezterm$': '',
'draw.io': '󰇟',
'firefox-developer-edition': '',
'google-chrome': '',
'title:YouTube ': '',
Spotify: '󰓇',
chromium: '',
code: '󰨞',
dbeaver: '',
edge: '󰇩',
evince: '',
firefox: '',
foot: '',
keepassxc: '',
keymapp: '',
kitty: '',
obsidian: '󰠮',
password$: '',
qBittorrent$: '',
rofi: '',
slack: '',
spotube: '󰓇',
steam: '',
telegram: '',
vlc: '󰕼',
};

144
src/lib/icons/icons.ts Normal file
View File

@@ -0,0 +1,144 @@
export const substitutes = {
'transmission-gtk': 'transmission',
'blueberry.py': 'blueberry',
Caprine: 'facebook-messenger',
'com.raggesilver.BlackBox-symbolic': 'terminal-symbolic',
'org.wezfurlong.wezterm-symbolic': 'terminal-symbolic',
'audio-headset-bluetooth': 'audio-headphones-symbolic',
'audio-card-analog-usb': 'audio-speakers-symbolic',
'audio-card-analog-pci': 'audio-card-symbolic',
'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',
nix: {
nix: 'nix-snowflake-symbolic',
},
app: {
terminal: 'terminal-symbolic',
},
fallback: {
executable: 'application-x-executable',
notification: 'dialog-information-symbolic',
video: 'video-x-generic-symbolic',
audio: 'audio-x-generic-symbolic',
},
ui: {
close: 'window-close-symbolic',
colorpicker: 'color-select-symbolic',
info: 'info-symbolic',
link: 'external-link-symbolic',
lock: 'system-lock-screen-symbolic',
menu: 'open-menu-symbolic',
refresh: 'view-refresh-symbolic',
search: 'system-search-symbolic',
settings: 'emblem-system-symbolic',
themes: 'preferences-desktop-theme-symbolic',
tick: 'object-select-symbolic',
time: 'hourglass-symbolic',
toolbars: 'toolbars-symbolic',
warning: 'dialog-warning-symbolic',
arrow: {
right: 'pan-end-symbolic',
left: 'pan-start-symbolic',
down: 'pan-down-symbolic',
up: 'pan-up-symbolic',
},
},
audio: {
mic: {
muted: 'microphone-disabled-symbolic',
low: 'microphone-sensitivity-low-symbolic',
medium: 'microphone-sensitivity-medium-symbolic',
high: 'microphone-sensitivity-high-symbolic',
},
volume: {
muted: 'audio-volume-muted-symbolic',
low: 'audio-volume-low-symbolic',
medium: 'audio-volume-medium-symbolic',
high: 'audio-volume-high-symbolic',
overamplified: 'audio-volume-overamplified-symbolic',
},
type: {
headset: 'audio-headphones-symbolic',
speaker: 'audio-speakers-symbolic',
card: 'audio-card-symbolic',
},
mixer: 'mixer-symbolic',
},
powerprofile: {
balanced: 'power-profile-balanced-symbolic',
'power-saver': 'power-profile-power-saver-symbolic',
performance: 'power-profile-performance-symbolic',
},
asusctl: {
profile: {
Balanced: 'power-profile-balanced-symbolic',
Quiet: 'power-profile-power-saver-symbolic',
Performance: 'power-profile-performance-symbolic',
},
mode: {
Integrated: 'processor-symbolic',
Hybrid: 'controller-symbolic',
},
},
battery: {
charging: 'battery-flash-symbolic',
warning: 'battery-empty-symbolic',
},
bluetooth: {
enabled: 'bluetooth-active-symbolic',
disabled: 'bluetooth-disabled-symbolic',
},
brightness: {
indicator: 'display-brightness-symbolic',
keyboard: 'keyboard-brightness-symbolic',
screen: 'display-brightness-symbolic',
},
powermenu: {
sleep: 'weather-clear-night-symbolic',
reboot: 'system-reboot-symbolic',
logout: 'system-log-out-symbolic',
shutdown: 'system-shutdown-symbolic',
},
recorder: {
recording: 'media-record-symbolic',
},
notifications: {
noisy: 'org.gnome.Settings-notifications-symbolic',
silent: 'notifications-disabled-symbolic',
message: 'chat-bubbles-symbolic',
},
trash: {
full: 'user-trash-full-symbolic',
empty: 'user-trash-symbolic',
},
mpris: {
shuffle: {
enabled: 'media-playlist-shuffle-symbolic',
disabled: 'media-playlist-consecutive-symbolic',
},
loop: {
none: 'media-playlist-repeat-symbolic',
track: 'media-playlist-repeat-song-symbolic',
playlist: 'media-playlist-repeat-symbolic',
},
playing: 'media-playback-pause-symbolic',
paused: 'media-playback-start-symbolic',
stopped: 'media-playback-start-symbolic',
prev: 'media-skip-backward-symbolic',
next: 'media-skip-forward-symbolic',
},
system: {
cpu: 'org.gnome.SystemMonitor-symbolic',
ram: 'drive-harddisk-solidstate-symbolic',
temp: 'temperature-symbolic',
},
color: {
dark: 'dark-mode-symbolic',
light: 'light-mode-symbolic',
},
};

198
src/lib/icons/icons2.ts Normal file
View File

@@ -0,0 +1,198 @@
export const substitutes = {
'transmission-gtk': 'transmission',
'blueberry.py': 'blueberry',
Caprine: 'facebook-messenger',
'com.raggesilver.BlackBox-symbolic': 'terminal-symbolic',
'org.wezfurlong.wezterm-symbolic': 'terminal-symbolic',
'audio-headset-bluetooth': 'audio-headphones-symbolic',
'audio-card-analog-usb': 'audio-speakers-symbolic',
'audio-card-analog-pci': 'audio-card-symbolic',
'preferences-system': 'emblem-system-symbolic',
'com.github.Aylur.ags-symbolic': 'controls-symbolic',
'com.github.Aylur.ags': 'controls-symbolic',
};
export default {
missing: 'image-missing-symbolic',
nix: {
nix: 'nix-snowflake-symbolic',
},
app: {
terminal: 'terminal-symbolic',
},
fallback: {
executable: 'application-x-executable',
notification: 'dialog-information-symbolic',
video: 'video-x-generic-symbolic',
audio: 'audio-x-generic-symbolic',
},
ui: {
close: 'window-close-symbolic',
colorpicker: 'color-select-symbolic',
info: 'info-symbolic',
link: 'external-link-symbolic',
lock: 'system-lock-screen-symbolic',
menu: 'open-menu-symbolic',
refresh: 'view-refresh-symbolic',
search: 'system-search-symbolic',
settings: 'emblem-system-symbolic',
themes: 'preferences-desktop-theme-symbolic',
tick: 'object-select-symbolic',
time: 'hourglass-symbolic',
toolbars: 'toolbars-symbolic',
warning: 'dialog-warning-symbolic',
arrow: {
right: 'pan-end-symbolic',
left: 'pan-start-symbolic',
down: 'pan-down-symbolic',
up: 'pan-up-symbolic',
},
},
audio: {
mic: {
muted: 'microphone-disabled-symbolic',
low: 'microphone-sensitivity-low-symbolic',
medium: 'microphone-sensitivity-medium-symbolic',
high: 'microphone-sensitivity-high-symbolic',
},
volume: {
muted: 'audio-volume-muted-symbolic',
low: 'audio-volume-low-symbolic',
medium: 'audio-volume-medium-symbolic',
high: 'audio-volume-high-symbolic',
overamplified: 'audio-volume-overamplified-symbolic',
},
type: {
headset: 'audio-headphones-symbolic',
speaker: 'audio-speakers-symbolic',
card: 'audio-card-symbolic',
},
mixer: 'mixer-symbolic',
},
powerprofile: {
balanced: 'power-profile-balanced-symbolic',
'power-saver': 'power-profile-power-saver-symbolic',
performance: 'power-profile-performance-symbolic',
},
asusctl: {
profile: {
Balanced: 'power-profile-balanced-symbolic',
Quiet: 'power-profile-power-saver-symbolic',
Performance: 'power-profile-performance-symbolic',
},
mode: {
Integrated: 'processor-symbolic',
Hybrid: 'controller-symbolic',
},
},
battery: {
charging: 'battery-flash-symbolic',
warning: 'battery-empty-symbolic',
},
bluetooth: {
enabled: 'bluetooth-active-symbolic',
disabled: 'bluetooth-disabled-symbolic',
},
brightness: {
indicator: 'display-brightness-symbolic',
keyboard: 'keyboard-brightness-symbolic',
screen: 'display-brightness-symbolic',
},
powermenu: {
sleep: 'weather-clear-night-symbolic',
reboot: 'system-reboot-symbolic',
logout: 'system-log-out-symbolic',
shutdown: 'system-shutdown-symbolic',
},
recorder: {
recording: 'media-record-symbolic',
},
notifications: {
noisy: 'org.gnome.Settings-notifications-symbolic',
silent: 'notifications-disabled-symbolic',
message: 'chat-bubbles-symbolic',
},
trash: {
full: 'user-trash-full-symbolic',
empty: 'user-trash-symbolic',
},
mpris: {
shuffle: {
enabled: 'media-playlist-shuffle-symbolic',
disabled: 'media-playlist-consecutive-symbolic',
},
loop: {
none: 'media-playlist-repeat-symbolic',
track: 'media-playlist-repeat-song-symbolic',
playlist: 'media-playlist-repeat-symbolic',
},
playing: 'media-playback-pause-symbolic',
paused: 'media-playback-start-symbolic',
stopped: 'media-playback-start-symbolic',
prev: 'media-skip-backward-symbolic',
next: 'media-skip-forward-symbolic',
},
system: {
cpu: 'org.gnome.SystemMonitor-symbolic',
ram: 'drive-harddisk-solidstate-symbolic',
temp: 'temperature-symbolic',
},
color: {
dark: 'dark-mode-symbolic',
light: 'light-mode-symbolic',
},
weather: {
warning: 'dialog-warning-symbolic',
sunny: 'weather-clear-symbolic',
clear: 'weather-clear-night-symbolic',
partly_cloudy: 'weather-few-clouds-symbolic',
partly_cloudy_night: 'weather-few-clouds-night-symbolic',
cloudy: 'weather-overcast-symbolic',
overcast: 'weather-overcast-symbolic',
mist: 'weather-overcast-symbolic',
patchy_rain_nearby: 'weather-showers-scattered-symbolic',
patchy_rain_possible: 'weather-showers-scattered-symbolic',
patchy_snow_possible: 'weather-snow-symbolic',
patchy_sleet_possible: 'weather-snow-symbolic',
patchy_freezing_drizzle_possible: 'weather-showers-scattered-symbolic',
thundery_outbreaks_possible: 'weather-overcast-symbolic',
blowing_snow: 'weather-snow-symbolic',
blizzard: 'weather-snow-symbolic',
fog: 'weather-fog-symbolic',
freezing_fog: 'weather-fog-symbolic',
patchy_light_drizzle: 'weather-showers-scattered-symbolic',
light_drizzle: 'weather-showers-symbolic',
freezing_drizzle: 'weather-showers-symbolic',
heavy_freezing_drizzle: 'weather-showers-symbolic',
patchy_light_rain: 'weather-showers-scattered-symbolic',
light_rain: 'weather-showers-symbolic',
moderate_rain_at_times: 'weather-showers-symbolic',
moderate_rain: 'weather-showers-symbolic',
heavy_rain_at_times: 'weather-showers-symbolic',
heavy_rain: 'weather-showers-symbolic',
light_freezing_rain: 'weather-showers-symbolic',
moderate_or_heavy_freezing_rain: 'weather-showers-symbolic',
light_sleet: 'weather-snow-symbolic',
moderate_or_heavy_sleet: 'weather-snow-symbolic',
patchy_light_snow: 'weather-snow-symbolic',
light_snow: 'weather-snow-symbolic',
patchy_moderate_snow: 'weather-snow-symbolic',
moderate_snow: 'weather-snow-symbolic',
patchy_heavy_snow: 'weather-snow-symbolic',
heavy_snow: 'weather-snow-symbolic',
ice_pellets: 'weather-showers-symbolic',
light_rain_shower: 'weather-showers-symbolic',
moderate_or_heavy_rain_shower: 'weather-showers-symbolic',
torrential_rain_shower: 'weather-showers-symbolic',
light_sleet_showers: 'weather-showers-symbolic',
moderate_or_heavy_sleet_showers: 'weather-showers-symbolic',
light_snow_showers: 'weather-snow-symbolic',
moderate_or_heavy_snow_showers: 'weather-snow-symbolic',
light_showers_of_ice_pellets: 'weather-showers-symbolic',
moderate_or_heavy_showers_of_ice_pellets: 'weather-showers-symbolic',
patchy_light_rain_with_thunder: 'weather-showers-scattered-symbolic',
moderate_or_heavy_rain_with_thunder: 'weather-showers-symbolic',
patchy_light_snow_with_thunder: 'weather-snow-symbolic',
moderate_or_heavy_snow_with_thunder: 'weather-snow-symbolic',
},
} as const;

54
src/lib/icons/weather.ts Normal file
View File

@@ -0,0 +1,54 @@
export const weatherIcons = {
warning: '󰼯',
sunny: '󰖙',
clear: '󰖔',
partly_cloudy: '󰖕',
partly_cloudy_night: '󰼱',
cloudy: '󰖐',
overcast: '󰖕',
mist: '󰖑',
patchy_rain_nearby: '󰼳',
patchy_rain_possible: '󰼳',
patchy_snow_possible: '󰼴',
patchy_sleet_possible: '󰙿',
patchy_freezing_drizzle_possible: '󰙿',
thundery_outbreaks_possible: '󰙾',
blowing_snow: '󰼶',
blizzard: '󰼶',
fog: '󰖑',
freezing_fog: '󰖑',
patchy_light_drizzle: '󰼳',
light_drizzle: '󰼳',
freezing_drizzle: '󰙿',
heavy_freezing_drizzle: '󰙿',
patchy_light_rain: '󰼳',
light_rain: '󰼳',
moderate_rain_at_times: '󰖗',
moderate_rain: '󰼳',
heavy_rain_at_times: '󰖖',
heavy_rain: '󰖖',
light_freezing_rain: '󰙿',
moderate_or_heavy_freezing_rain: '󰙿',
light_sleet: '󰙿',
moderate_or_heavy_sleet: '󰙿',
patchy_light_snow: '󰼴',
light_snow: '󰼴',
patchy_moderate_snow: '󰼴',
moderate_snow: '󰼶',
patchy_heavy_snow: '󰼶',
heavy_snow: '󰼶',
ice_pellets: '󰖒',
light_rain_shower: '󰖖',
moderate_or_heavy_rain_shower: '󰖖',
torrential_rain_shower: '󰖖',
light_sleet_showers: '󰼵',
moderate_or_heavy_sleet_showers: '󰼵',
light_snow_showers: '󰼵',
moderate_or_heavy_snow_showers: '󰼵',
light_showers_of_ice_pellets: '󰖒',
moderate_or_heavy_showers_of_ice_pellets: '󰖒',
patchy_light_rain_with_thunder: '󰙾',
moderate_or_heavy_rain_with_thunder: '󰙾',
patchy_light_snow_with_thunder: '󰼶',
moderate_or_heavy_snow_with_thunder: '󰼶',
} as const;

321
src/lib/option.ts Normal file
View File

@@ -0,0 +1,321 @@
import { isHexColor } from '../globals/variables';
import { MkOptionsResult } from './types/options';
import { ensureDirectory } from './session';
import Variable from 'astal/variable';
import { monitorFile, readFile, writeFile } from 'astal/file';
import GLib from 'gi://GLib?version=2.0';
type OptProps = {
persistent?: boolean;
};
/**
* A file to store default configurations. Placed inside the cache directory.
* NOTE: We need to move this out into the .config directory instead.
*/
export const defaultFile = `${GLib.get_tmp_dir()}/ags/hyprpanel/default.json`;
export class Opt<T = unknown> extends Variable<T> {
/**
* The initial value set when the `Opt` is created.
*/
public readonly initial: T;
/**
* Indicates whether this option should remain unchanged even when reset operations occur.
*/
public readonly persistent: boolean;
private _id = '';
/**
* Creates an instance of `Opt`.
*
* @param {T} initial - The initial value of the option.
* @param {OptProps} [props={}] - Additional properties for the option.
*/
constructor(initial: T, { persistent = false }: OptProps = {}) {
super(initial);
this.initial = initial;
this.persistent = persistent;
}
/**
* Converts the current value to a JSON-compatible string.
*
* @returns {string}
*/
toJSON(): string {
return `opt:${JSON.stringify(this.get())}`;
}
public get value(): T {
return this.get();
}
/**
* Setter for the current value of the option.
*/
public set value(val: T) {
this.set(val);
}
/**
* Getter for the unique ID of the option.
*/
public get id(): string {
return this._id;
}
/**
* Setter for the unique ID of the option.
*/
public set id(newId: string) {
this._id = newId;
}
/**
* Initializes this option by attempting to read its value from a cache file.
* If found, sets the current value. Also sets up a subscription to write updates back.
*
* @param cacheFile - The path to the cache file.
*/
public init(cacheFile: string): void {
const rawData = readFile(cacheFile);
let cacheData: Record<string, unknown> = {};
if (rawData && rawData.trim() !== '') {
try {
cacheData = JSON.parse(rawData) as Record<string, unknown>;
} catch {
// do nuffin
}
}
const cachedVariable = cacheData[this._id];
if (cachedVariable !== undefined) {
this.set(cachedVariable as T);
}
this.subscribe((newVal) => {
const reRaw = readFile(cacheFile);
let currentCache: Record<string, unknown> = {};
if (reRaw && reRaw.trim() !== '') {
try {
currentCache = JSON.parse(reRaw) as Record<string, unknown>;
} catch {
// Do nuffin
}
}
currentCache[this._id] = newVal;
writeFile(cacheFile, JSON.stringify(currentCache, null, 2));
});
}
/**
* Initializes this option by attempting to read its default value from the default file.
* If found, sets the current value.
*/
public createDefault(): void {
const rawData = readFile(defaultFile);
let defaultData: Record<string, unknown> = {};
if (rawData && rawData.trim() !== '') {
try {
defaultData = JSON.parse(rawData) as Record<string, unknown>;
} catch {
// do nuffin
}
}
const defaultVal = defaultData[this._id];
if (defaultVal !== undefined) {
this.set(defaultVal as T);
}
}
/**
* Resets the value of this option to its initial value if not persistent and if it differs from the current value.
*
* @returns Returns the option's ID if reset occurred, otherwise undefined.
*/
public reset(): string | undefined {
if (this.persistent) {
return undefined;
}
const current = this.get();
if (JSON.stringify(current) !== JSON.stringify(this.initial)) {
this.set(this.initial);
return this._id;
}
return undefined;
}
}
/**
* Creates an `Opt` instance with the given initial value and properties.
* @template T
* @param initial - The initial value.
* @param [props] - Additional properties.
*/
export function opt<T>(initial: T, props?: OptProps): Opt<T> {
return new Opt(initial, props);
}
/**
* Recursively traverses the provided object to extract all `Opt` instances, assigning IDs to each.
*
* @param object - The object containing `Opt` instances.
* @param [path=''] - The current path (used internally).
* @param [arr=[]] - The accumulator array for found `Opt` instances.
* @returns An array of all found `Opt` instances.
*/
function getOptions(object: Record<string, unknown>, path = '', arr: Opt[] = []): Opt[] {
for (const key in object) {
const value = object[key];
const id = path ? `${path}.${key}` : key;
if (value instanceof Variable) {
const optValue = value as Opt;
optValue.id = id;
arr.push(optValue);
} else if (typeof value === 'object' && value !== null) {
getOptions(value as Record<string, unknown>, id, arr);
}
}
return arr;
}
/**
* Creates and initializes options from a given object structure. The returned object
* includes methods to reset values, reset theme colors, and handle dependencies.
*
* @template T extends object
* @param cacheFile - The file path to store cached values.
* @param object - The object containing nested `Opt` instances.
* @param [confFile='config.json'] - The configuration file name stored in TMP.
* @returns The original object extended with additional methods for handling options.
*/
export function mkOptions<T extends object>(
cacheFile: string,
object: T,
confFile: string = 'config.json',
): T & MkOptionsResult {
const allOptions = getOptions(object as Record<string, unknown>);
for (let i = 0; i < allOptions.length; i++) {
allOptions[i].init(cacheFile);
}
ensureDirectory(cacheFile.split('/').slice(0, -1).join('/'));
ensureDirectory(defaultFile.split('/').slice(0, -1).join('/'));
const configFile = `${TMP}/${confFile}`;
const values: Record<string, unknown> = {};
const defaultValues: Record<string, unknown> = {};
for (let i = 0; i < allOptions.length; i++) {
const option = allOptions[i];
const val = option.value;
values[option.id] = val;
if (isHexColor(val as string)) {
defaultValues[option.id] = option.initial;
} else {
defaultValues[option.id] = val;
}
}
writeFile(defaultFile, JSON.stringify(defaultValues, null, 2));
writeFile(configFile, JSON.stringify(values, null, 2));
monitorFile(configFile, () => {
const raw = readFile(configFile);
if (!raw || raw.trim() === '') return;
let cache: Record<string, unknown>;
try {
cache = JSON.parse(raw) as Record<string, unknown>;
} catch {
return;
}
for (let i = 0; i < allOptions.length; i++) {
const opt = allOptions[i];
const newVal = cache[opt.id];
const oldVal = opt.get();
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
opt.set(newVal as T);
}
}
});
/**
* A simple sleep utility.
*
* @param [ms=0] - Milliseconds to sleep.
*/
function sleep(ms = 0): Promise<T> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Resets all options to their initial values if possible.
*
* @param opts - Array of all option instances.
* @returns IDs of all reset options.
*/
async function resetAll(opts: Opt[]): Promise<string[]> {
const results: string[] = [];
for (let i = 0; i < opts.length; i++) {
const id = opts[i].reset();
if (id) {
results.push(id);
await sleep(50);
}
}
return results;
}
return Object.assign(object, {
configFile,
array: (): Opt[] => allOptions,
async reset(): Promise<string> {
const ids = await resetAll(allOptions);
return ids.join('\n');
},
/**
* Registers a callback that fires when any option whose ID starts with any of the given dependencies changes.
*
* @param deps - An array of dependency prefixes.
* @param callback - The callback function to execute on changes.
*/
handler(deps: string[], callback: () => void): void {
for (let i = 0; i < allOptions.length; i++) {
const opt = allOptions[i];
for (let j = 0; j < deps.length; j++) {
if (opt.id.startsWith(deps[j])) {
opt.subscribe(callback);
break;
}
}
}
},
});
}

View File

@@ -0,0 +1,90 @@
import { Bind } from 'src/lib/types/variable';
import { GenericFunction } from 'src/lib/types/customModules/generic';
import { BarModule } from 'src/lib/types/options';
import { Poller } from './Poller';
import { execAsync, Variable } from 'astal';
/**
* A class that manages polling of a variable by executing a bash command at specified intervals.
*/
export class BashPoller<Value, Parameters extends unknown[]> {
private poller: Poller;
private params: Parameters;
/**
* Creates an instance of BashPoller.
*
* @param targetVariable - The target variable to poll.
* @param trackers - An array of trackers to monitor.
* @param pollingInterval - The interval at which polling occurs.
* @param updateCommand - The command to update the target variable.
* @param pollingFunction - The function to execute during each poll.
* @param params - Additional parameters for the polling function.
*
* @example
*
* ```ts
* //##################### EXAMPLE ##########################
* const updatesPoller = new BashPoller<string, []>(
* pendingUpdates,
* [bind(padZero), bind(postInputUpdater)],
* bind(pollingInterval),
* updateCommand.value,
* processUpdateCount,
* );
* //#######################################################
*
* ```
*/
constructor(
private targetVariable: Variable<Value>,
private trackers: Bind[],
private pollingInterval: Bind,
private updateCommand: string,
private pollingFunction: GenericFunction<Value, [string, ...Parameters]>,
...params: Parameters
) {
this.params = params;
this.poller = new Poller(this.pollingInterval, this.trackers, this.execute);
}
/**
* Executes the bash command specified in the updateCommand property.
*
* The result of the command is processed by the pollingFunction and
* assigned to the targetVariable.
*/
public execute = async (): Promise<void> => {
try {
const res = await execAsync(`bash -c "${this.updateCommand}"`);
this.targetVariable.set(await this.pollingFunction(res, ...this.params));
} catch (error) {
console.error(`Error executing bash command "${this.updateCommand}":`, error);
}
};
/**
* Starts the polling process.
*/
public start(): void {
this.poller.start();
}
/**
* Stops the polling process.
*/
public stop(): void {
this.poller.stop();
}
/**
* Initializes the poller with the specified module.
*
* @param moduleName - The name of the module to initialize.
*/
public initialize(moduleName?: BarModule): void {
this.poller.initialize(moduleName);
}
}

View File

@@ -0,0 +1,86 @@
import { Bind } from 'src/lib/types/variable';
import { GenericFunction } from 'src/lib/types/customModules/generic';
import { BarModule } from 'src/lib/types/options';
import { Poller } from './Poller';
import { Variable } from 'astal';
/**
* A class that manages polling of a variable by executing a generic function at specified intervals.
*/
export class FunctionPoller<Value, Parameters extends unknown[] = []> {
private poller: Poller;
private params: Parameters;
/**
* Creates an instance of FunctionPoller.
*
* @param targetVariable - The target variable to poll.
* @param trackers - An array of trackers to monitor.
* @param pollingInterval - The interval at which polling occurs.
* @param pollingFunction - The function to execute during each poll.
* @param params - Additional parameters for the polling function.
*
* @example
*
* ```ts
* //##################### EXAMPLE ##########################
* const cpuPoller = new FunctionPoller<number, []>(
* cpuUsage,
* [bind(round)],
* bind(pollingInterval),
* computeCPU,
* );
* //#######################################################
*
* ```
*/
constructor(
private targetVariable: Variable<Value>,
private trackers: Bind[],
private pollingInterval: Bind,
private pollingFunction: GenericFunction<Value, Parameters>,
...params: Parameters
) {
this.params = params;
this.poller = new Poller(this.pollingInterval, this.trackers, this.execute);
}
/**
* Executes the polling function with the provided parameters.
*
* The result of the function is assigned to the target variable.
*/
private execute = async (): Promise<void> => {
try {
const result = await this.pollingFunction(...this.params);
this.targetVariable.set(result);
} catch (error) {
console.error('Error executing polling function:', error);
}
};
/**
* Starts the polling process.
*/
public start(): void {
this.poller.start();
}
/**
* Stops the polling process.
*/
public stop(): void {
this.poller.stop();
}
/**
* Initializes the poller with the specified module.
*
* @param moduleName - The name of the module to initialize.
*/
public initialize(moduleName?: BarModule): void {
this.poller.initialize(moduleName);
}
}

107
src/lib/poller/Poller.ts Normal file
View File

@@ -0,0 +1,107 @@
import { Bind } from 'src/lib/types/variable';
import { BarModule } from 'src/lib/types/options';
import { getLayoutItems } from 'src/lib/utils';
import { AstalIO, interval, Variable } from 'astal';
const { layouts } = options.bar;
/**
* A class that manages the polling lifecycle, including interval management and execution state.
*/
export class Poller {
private intervalInstance: AstalIO.Time | null = null;
private isExecuting: boolean = false;
private pollingFunction: () => Promise<void>;
/**
* Creates an instance of Poller.
* @param pollingInterval - The interval at which polling occurs.
* @param trackers - An array of trackers to monitor.
* @param pollingFunction - The function to execute during each poll.
*/
constructor(
private pollingInterval: Bind,
private trackers: Bind[],
pollingFunction: () => Promise<void>,
) {
this.pollingFunction = pollingFunction;
}
/**
* Starts the polling process by setting up the interval.
*/
public start(): void {
Variable.derive([this.pollingInterval, ...this.trackers], (intervalMs: number) => {
this.executePolling(intervalMs);
})();
}
/**
* Stops the polling process and cleans up resources.
*/
public stop(): void {
if (this.intervalInstance !== null) {
this.intervalInstance.cancel();
this.intervalInstance = null;
}
}
/**
* Initializes the polling based on module usage.
*
* If not module is provided then we can safely assume that we want
* to always run the pollig interval.
*
* @param moduleName - The name of the module to initialize.
*/
public initialize(moduleName?: BarModule): void {
if (moduleName === undefined) {
return this.start();
}
const initialModules = getLayoutItems();
if (initialModules.includes(moduleName)) {
this.start();
} else {
this.stop();
}
layouts.subscribe(() => {
const usedModules = getLayoutItems();
if (usedModules.includes(moduleName)) {
this.start();
} else {
this.stop();
}
});
}
/**
* Executes the polling function at the specified interval.
*
* @param intervalMs - The polling interval in milliseconds.
*/
private executePolling(intervalMs: number): void {
if (this.intervalInstance !== null) {
this.intervalInstance.cancel();
}
this.intervalInstance = interval(intervalMs, async () => {
if (this.isExecuting) {
return;
}
this.isExecuting = true;
try {
await this.pollingFunction();
} catch (error) {
console.error('Error during polling execution:', error);
} finally {
this.isExecuting = false;
}
});
}
}

24
src/lib/session.ts Normal file
View File

@@ -0,0 +1,24 @@
import { App } from 'astal/gtk3';
import { Gio } from 'astal/file';
import { GLib } from 'astal/gobject';
declare global {
const CONFIG: string;
const TMP: string;
const USER: string;
const SRC_DIR: string;
}
export function ensureDirectory(path: string): void {
if (!GLib.file_test(path, GLib.FileTest.EXISTS)) Gio.File.new_for_path(path).make_directory_with_parents(null);
}
Object.assign(globalThis, {
CONFIG: `${GLib.get_user_config_dir()}/hyprpanel/config.json`,
TMP: `${GLib.get_tmp_dir()}/hyprpanel`,
USER: GLib.get_user_name(),
SRC_DIR: GLib.getenv('HYPRPANEL_DATADIR') ?? SRC,
});
ensureDirectory(TMP);
App.add_icons(`${SRC_DIR}/assets`);

View File

@@ -0,0 +1,95 @@
import { GtkWidget } from 'src/lib/types/widget.js';
import { Gdk } from 'astal/gtk3';
import { ThrottleFn } from '../types/utils';
/**
* Connects a primary click handler and returns a disconnect function.
*/
export function onPrimaryClick(widget: GtkWidget, handler: (self: GtkWidget, event: Gdk.Event) => void): () => void {
const id = widget.connect('button-press-event', (self: GtkWidget, event: Gdk.Event) => {
const eventButton = event.get_button()[1];
if (eventButton === Gdk.BUTTON_PRIMARY) {
handler(self, event);
}
});
return () => widget.disconnect(id);
}
/**
* Connects a secondary click handler and returns a disconnect function.
*/
export function onSecondaryClick(widget: GtkWidget, handler: (self: GtkWidget, event: Gdk.Event) => void): () => void {
const id = widget.connect('button-press-event', (self: GtkWidget, event: Gdk.Event) => {
const eventButton = event.get_button()[1];
if (eventButton === Gdk.BUTTON_SECONDARY) {
handler(self, event);
}
});
return () => widget.disconnect(id);
}
/**
* Connects a middle click handler and returns a disconnect function.
*/
export function onMiddleClick(widget: GtkWidget, handler: (self: GtkWidget, event: Gdk.Event) => void): () => void {
const id = widget.connect('button-press-event', (self: GtkWidget, event: Gdk.Event) => {
const eventButton = event.get_button()[1];
if (eventButton === Gdk.BUTTON_MIDDLE) {
handler(self, event);
}
});
return () => widget.disconnect(id);
}
/**
* Connects a scroll handler and returns a disconnect function.
*/
export function onScroll(
widget: GtkWidget,
throttledHandler: ThrottleFn,
scrollUpAction: string,
scrollDownAction: string,
): () => void {
const id = widget.connect('scroll-event', (self: GtkWidget, event: Gdk.Event) => {
const [directionSuccess, direction] = event.get_scroll_direction();
const [deltasSuccess, , yScroll] = event.get_scroll_deltas();
if (directionSuccess) {
handleScrollDirection(direction, scrollUpAction, scrollDownAction, self, event, throttledHandler);
} else if (deltasSuccess) {
handleScrollDeltas(yScroll, scrollUpAction, scrollDownAction, self, event, throttledHandler);
}
});
return () => widget.disconnect(id);
}
function handleScrollDirection(
direction: Gdk.ScrollDirection,
scrollUpAction: string,
scrollDownAction: string,
self: GtkWidget,
event: Gdk.Event,
throttledHandler: ThrottleFn,
): void {
if (direction === Gdk.ScrollDirection.UP) {
throttledHandler(scrollUpAction, { clicked: self, event });
} else if (direction === Gdk.ScrollDirection.DOWN) {
throttledHandler(scrollDownAction, { clicked: self, event });
}
}
function handleScrollDeltas(
yScroll: number,
scrollUpAction: string,
scrollDownAction: string,
self: GtkWidget,
event: Gdk.Event,
throttledHandler: ThrottleFn,
): void {
if (yScroll > 0) {
throttledHandler(scrollDownAction, { clicked: self, event });
} else if (yScroll < 0) {
throttledHandler(scrollUpAction, { clicked: self, event });
}
}

View File

@@ -0,0 +1,47 @@
import { Connectable, Subscribable } from 'astal/binding';
import { Widget } from 'astal/gtk3';
/**
* A generic hook utility to manage setup and teardown based on dependencies.
*
* @param widget - The GtkWidget instance.
* @param hookTarget - The object to hook into (Connectable or Subscribable).
* @param setup - The setup function to execute, which returns a disconnect function.
* @param signal - (Optional) The signal name if hooking into a Connectable.
*/
export function useHook(
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
widget: any,
hookTarget: Connectable | Subscribable,
setup: (() => void) | (() => () => void),
signal?: string,
): void {
const passedWidget: Widget.Box = widget;
let currentDisconnect: () => void = () => {};
const executeSetup = (): void => {
currentDisconnect();
if (typeof setup === 'function') {
currentDisconnect = setup() || ((): void => {});
}
};
const isConnectable = (target: Connectable | Subscribable): target is Connectable => {
return 'connect' in target;
};
const isSubscribable = (target: Connectable | Subscribable): target is Subscribable => {
return 'subscribe' in target;
};
const hookIntoTarget = (): void => {
if (signal && isConnectable(hookTarget)) {
passedWidget.hook(hookTarget, signal, executeSetup);
} else if (isSubscribable(hookTarget)) {
passedWidget.hook(hookTarget, executeSetup);
}
};
executeSetup();
hookIntoTarget();
}

33
src/lib/shared/media.ts Normal file
View File

@@ -0,0 +1,33 @@
import AstalMpris from 'gi://AstalMpris?version=0.1';
import { mprisService } from '../constants/services';
export const getCurrentPlayer = (
activePlayer: AstalMpris.Player = mprisService.get_players()[0],
): AstalMpris.Player => {
const statusOrder = {
[AstalMpris.PlaybackStatus.PLAYING]: 1,
[AstalMpris.PlaybackStatus.PAUSED]: 2,
[AstalMpris.PlaybackStatus.STOPPED]: 3,
};
const mprisPlayers = mprisService.get_players();
if (mprisPlayers.length === 0) {
return mprisPlayers[0];
}
const isPlaying = mprisPlayers.some(
(p: AstalMpris.Player) => p.playbackStatus === AstalMpris.PlaybackStatus.PLAYING,
);
const playerStillExists = mprisPlayers.some((p) => activePlayer.bus_name === p.bus_name);
const nextPlayerUp = mprisPlayers.sort(
(a: AstalMpris.Player, b: AstalMpris.Player) => statusOrder[a.playbackStatus] - statusOrder[b.playbackStatus],
)[0];
if (isPlaying || !playerStillExists) {
return nextPlayerUp;
}
return activePlayer;
};

View File

@@ -0,0 +1,21 @@
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
const normalizeName = (name: string): string => name.toLowerCase().replace(/\s+/g, '_');
export const isNotificationIgnored = (notification: AstalNotifd.Notification, filter: string[]): boolean => {
const notificationFilters = new Set(filter.map(normalizeName));
const normalizedAppName = normalizeName(notification.app_name);
return notificationFilters.has(normalizedAppName);
};
export const filterNotifications = (
notifications: AstalNotifd.Notification[],
filter: string[],
): AstalNotifd.Notification[] => {
const filteredNotifications = notifications.filter((notif: AstalNotifd.Notification) => {
return !isNotificationIgnored(notif, filter);
});
return filteredNotifications;
};

11
src/lib/types/astal-extensions.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import { Gdk } from 'astal/gtk3';
declare module 'astal/gtk3' {
interface EventButton extends Gdk.Event {
get_root_coords(): [number, number];
}
interface EventScroll extends Gdk.Event {
direction: Gdk.ScrollDirection;
}
}

14
src/lib/types/audio.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
export type InputDevices = Button<Box<Box<Label<Attribute>, Attribute>, Attribute>, Attribute>[];
type DummyDevices = Button<Box<Box<Label<Attribute>, Attribute>, Attribute>, Attribute>[];
type RealPlaybackDevices = Button<Box<Box<Label<Attribute>, Attribute>, Attribute>, Attribute>[];
export type PlaybackDevices = DummyDevices | RealPlaybackDevices;
export type MediaTags = {
title: string;
artists: string;
artist: string;
album: string;
name: string;
identity: string;
};

42
src/lib/types/bar.d.ts vendored Normal file
View File

@@ -0,0 +1,42 @@
import { Binding, Connectable } from 'types/service';
import { Variable } from 'types/variable';
import Box from 'types/widgets/box';
import Button, { ButtonProps } from 'types/widgets/button';
import Label from 'types/widgets/label';
import { Attribute, Child } from './widget';
import { Widget } from 'astal/gtk3';
export type BarBoxChild = {
component: JSX.Element;
isVisible?: boolean;
isVis?: Variable<boolean>;
isBox?: boolean;
boxClass: string;
tooltip_text?: string | Binding<string>;
} & ({ isBox: true; props: Widget.BoxProps } | { isBox?: false; props: Widget.ButtonProps });
export type SelfButton = Button<Child, Attribute>;
export type BoxHook = (self: Box<Gtk.Widget, Gtk.Widget>) => void;
export type LabelHook = (self: Label<Gtk.Widget>) => void;
export type BarModule = {
icon?: string | Binding<string>;
textIcon?: string | Binding<string>;
useTextIcon?: Binding<boolean>;
label?: string | Binding<string>;
labelHook?: LabelHook;
boundLabel?: string;
tooltipText?: string | Binding<string>;
boxClass: string;
props?: Widget.ButtonProps;
showLabel?: boolean;
showLabelBinding?: Binding;
hook?: BoxHook;
connection?: Binding<Connectable>;
};
export type ResourceLabelType = 'used/total' | 'used' | 'percentage' | 'free';
export type NetstatLabelType = 'full' | 'in' | 'out';
export type RateUnit = 'GiB' | 'MiB' | 'KiB' | 'auto';

5
src/lib/types/battery.ts Normal file
View File

@@ -0,0 +1,5 @@
export type BatteryIconKeys = 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100;
export type BatteryIcons = {
[key in BatteryIconKeys]: string;
};

View File

@@ -0,0 +1,20 @@
export type GenericFunction<Value, Parameters extends unknown[]> = (...args: Parameters) => Promise<Value> | Value;
export type GenericResourceMetrics = {
total: number;
used: number;
percentage: number;
};
export type GenericResourceData = GenericResourceMetrics & {
free: number;
};
export type Postfix = 'TiB' | 'GiB' | 'MiB' | 'KiB' | 'B';
export type UpdateHandlers = {
disconnectPrimary: () => void;
disconnectSecondary: () => void;
disconnectMiddle: () => void;
disconnectScroll: () => void;
};

View File

@@ -0,0 +1,32 @@
import { layoutMap } from 'src/components/bar/modules/kblayout/layouts';
export type KbLabelType = 'layout' | 'code';
export type HyprctlKeyboard = {
address: string;
name: string;
rules: string;
model: string;
layout: string;
variant: string;
options: string;
active_keymap: string;
main: boolean;
};
export type HyprctlMouse = {
address: string;
name: string;
defaultSpeed: number;
};
export type HyprctlDeviceLayout = {
mice: HyprctlMouse[];
keyboards: HyprctlKeyboard[];
tablets: unknown[];
touch: unknown[];
switches: unknown[];
};
export type LayoutKeys = keyof typeof layoutMap;
export type LayoutValues = (typeof layoutMap)[LayoutKeys];

View File

@@ -0,0 +1,4 @@
export type NetworkResourceData = {
in: string;
out: string;
};

17
src/lib/types/customModules/utils.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import { Binding } from 'src/lib/utils';
import { Variable } from 'types/variable';
export type InputHandlerEvents = {
onPrimaryClick?: Binding;
onSecondaryClick?: Binding;
onMiddleClick?: Binding;
onScrollUp?: Binding;
onScrollDown?: Binding;
};
export type RunAsyncCommand = (
cmd: string,
args: EventArgs,
fn?: (output: string) => void,
postInputUpdater?: Variable<boolean>,
) => void;

15
src/lib/types/dashboard.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
export type ShortcutFixed = {
tooltip: string;
command: string;
icon: string;
configurable: false;
};
export type ShortcutVariable = {
tooltip: VarType<string>;
command: VarType<string>;
icon: VarType<string>;
configurable?: true;
};
export type Shortcut = ShortcutFixed | ShortcutVariable;

View File

@@ -0,0 +1,15 @@
import { Astal } from 'astal/gtk3';
import { NetstatLabelType, ResourceLabelType } from '../bar';
import { BarLocation } from '../options';
export const LABEL_TYPES: ResourceLabelType[] = ['used/total', 'used', 'free', 'percentage'];
export const NETWORK_LABEL_TYPES: NetstatLabelType[] = ['full', 'in', 'out'];
type LocationMap = {
[key in BarLocation]: Astal.WindowAnchor;
};
export const locationMap: LocationMap = {
top: Astal.WindowAnchor.TOP,
bottom: Astal.WindowAnchor.BOTTOM,
};

View File

@@ -0,0 +1,10 @@
import { RateUnit } from '../bar';
import { NetworkResourceData } from '../customModules/network';
export const GET_DEFAULT_NETSTAT_DATA = (dataType: RateUnit): NetworkResourceData => {
if (dataType === 'auto') {
return { in: `0 Kib/s`, out: `0 Kib/s` };
}
return { in: `0 ${dataType}/s`, out: `0 ${dataType}/s` };
};

View File

@@ -0,0 +1,60 @@
export const defaultColorMap = {
rosewater: '#f5e0dc',
flamingo: '#f2cdcd',
pink: '#f5c2e7',
mauve: '#cba6f7',
red: '#f38ba8',
maroon: '#eba0ac',
peach: '#fab387',
yellow: '#f9e2af',
green: '#a6e3a1',
teal: '#94e2d5',
sky: '#89dceb',
sapphire: '#74c7ec',
blue: '#89b4fa',
lavender: '#b4befe',
text: '#cdd6f4',
subtext1: '#bac2de',
subtext2: '#a6adc8',
overlay2: '#9399b2',
overlay1: '#7f849c',
overlay0: '#6c7086',
surface2: '#585b70',
surface1: '#45475a',
surface0: '#313244',
base2: '#242438',
base: '#1e1e2e',
mantle: '#181825',
crust: '#11111b',
surface1_2: '#454759',
text2: '#cdd6f3',
pink2: '#f5c2e6',
red2: '#f38ba7',
peach2: '#fab386',
mantle2: '#181824',
surface0_2: '#313243',
surface2_2: '#585b69',
overlay1_2: '#7f849b',
lavender2: '#b4befd',
mauve2: '#cba6f6',
green2: '#a6e3a0',
sky2: '#89dcea',
teal2: '#94e2d4',
yellow2: '#f9e2ad',
maroon2: '#eba0ab',
crust2: '#11111a',
pink3: '#f5c2e8',
red3: '#f38ba9',
mantle3: '#181826',
surface0_3: '#313245',
surface2_3: '#585b71',
overlay1_3: '#7f849d',
lavender3: '#b4beff',
mauve3: '#cba6f8',
green3: '#a6e3a2',
sky3: '#89dcec',
teal3: '#94e2d6',
yellow3: '#f9e2ae',
maroon3: '#eba0ad',
crust3: '#11111c',
} as const;

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,15 @@
import { GtkWidget, Transition } from './widget';
import { Astal, Gtk } from 'astal/gtk3';
import { WindowProps } from 'astal/gtk3/widget';
import { Opt } from '../option';
import { Binding } from 'astal';
import { BindableChild } from 'astal/gtk3/astalify';
export interface DropdownMenuProps extends WindowProps {
name: string;
child?: BindableChild | BindableChild[];
layout?: string;
transition?: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>;
exclusivity?: Astal.Exclusivity;
fixed?: boolean;
}

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

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

4
src/lib/types/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export type MousePos = {
source: string;
pos: number[];
};

25
src/lib/types/gpustat.d.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
export type GPUStatProcess = {
username: string;
command: string;
full_command: string[];
gpu_memory_usage: number;
cpu_percent: number;
cpu_memory_usage: number;
pid: number;
};
export type GPUStat = {
index: number;
uuid: string;
name: string;
'temperature.gpu': number;
'fan.speed': number;
'utilization.gpu': number;
'utilization.enc': number;
'utilization.dec': number;
'power.draw': number;
'enforced.power.limit': number;
'memory.used': number;
'memory.total': number;
processes: Process[];
};

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

@@ -0,0 +1,6 @@
import AstalMpris from 'gi://AstalMpris?version=0.1';
import icons2 from '../icons/icons2';
export type PlaybackIconMap = {
[key in AstalMpris.PlaybackStatus]: string;
};

16
src/lib/types/network.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
import { WIFI_STATUS_MAP } from 'src/globals/network';
export type AccessPoint = {
bssid: string | null;
address: string | null;
lastSeen: number;
ssid: string | null;
active: boolean;
strength: number;
frequency: number;
iconName: string | undefined;
};
export type WifiStatus = keyof typeof WIFI_STATUS_MAP;
export type WifiIcon = '󰤩' | '󰤨' | '󰤪' | '󰤨' | '󰤩' | '󰤮' | '󰤨' | '󰤥' | '󰤢' | '󰤟' | '󰤯';

15
src/lib/types/notification.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
import icons from 'src/lib/icons/icons2';
export interface NotificationArgs {
appName?: string;
body?: string;
iconName?: string;
id?: number;
summary?: string;
urgency?: Urgency;
category?: string;
timeout?: number;
transient?: boolean;
}
export type NotificationIcon = keyof typeof icons.notifications;

268
src/lib/types/options.d.ts vendored Normal file
View File

@@ -0,0 +1,268 @@
import { Opt } from 'src/lib/option';
import { Variable } from 'types/variable';
import { defaultColorMap } from './defaults/options';
import { Astal } from 'astal/gtk3';
import { dropdownMenuList } from '../constants/options';
export type MkOptionsResult = {
configFile: string;
array: () => Opt[];
reset: () => Promise<string>;
handler: (deps: string[], callback: () => void) => void;
};
export type RecursiveOptionsObject = {
[key: string]:
| RecursiveOptionsObject
| Opt<string>
| Opt<number>
| Opt<boolean>
| Variable<string>
| Variable<number>
| Variable<boolean>;
};
export type BarLocation = 'top' | 'bottom';
export type AutoHide = 'never' | 'fullscreen' | 'single-window';
export type BarModule =
| 'battery'
| 'dashboard'
| 'workspaces'
| 'windowtitle'
| 'media'
| 'notifications'
| 'volume'
| 'network'
| 'bluetooth'
| 'clock'
| 'ram'
| 'cpu'
| 'cputemp'
| 'storage'
| 'netstat'
| 'kbinput'
| 'updates'
| 'submap'
| 'weather'
| 'power'
| 'systray'
| 'hypridle'
| 'hyprsunset';
export type BarLayout = {
left: BarModule[];
middle: BarModule[];
right: BarModule[];
};
export type BarLayouts = {
[key: string]: BarLayout;
};
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';
export type OSDAnchor = 'top left' | 'top' | 'top right' | 'right' | 'bottom right' | 'bottom' | 'bottom left' | 'left';
export type BarButtonStyles = 'default' | 'split' | 'wave' | 'wave2';
export type ThemeExportData = {
filePath: string;
themeOnly: boolean;
};
export type InputType =
| 'number'
| 'color'
| 'float'
| 'object'
| 'string'
| 'enum'
| 'boolean'
| 'img'
| 'wallpaper'
| 'export'
| 'import'
| 'config_import'
| 'font';
export interface RowProps<T> {
opt: Opt<T>;
note?: string;
type?: InputType;
enums?: T[];
max?: number;
min?: number;
disabledBinding?: Variable<boolean>;
exportData?: ThemeExportData;
subtitle?: string | VarType<string> | Opt;
subtitleLink?: string;
dependencies?: string[];
increment?: number;
}
export type OSDOrientation = 'horizontal' | 'vertical';
export type HexColor = `#${string}`;
export type WindowLayer = 'top' | 'bottom' | 'overlay' | 'background';
export type ActiveWsIndicator = 'underline' | 'highlight' | 'color';
export type MatugenColors = {
background: HexColor;
error: HexColor;
error_container: HexColor;
inverse_on_surface: HexColor;
inverse_primary: HexColor;
inverse_surface: HexColor;
on_background: HexColor;
on_error: HexColor;
on_error_container: HexColor;
on_primary: HexColor;
on_primary_container: HexColor;
on_primary_fixed: HexColor;
on_primary_fixed_variant: HexColor;
on_secondary: HexColor;
on_secondary_container: HexColor;
on_secondary_fixed: HexColor;
on_secondary_fixed_variant: HexColor;
on_surface: HexColor;
on_surface_variant: HexColor;
on_tertiary: HexColor;
on_tertiary_container: HexColor;
on_tertiary_fixed: HexColor;
on_tertiary_fixed_variant: HexColor;
outline: HexColor;
outline_variant: HexColor;
primary: HexColor;
primary_container: HexColor;
primary_fixed: HexColor;
primary_fixed_dim: HexColor;
scrim: HexColor;
secondary: HexColor;
secondary_container: HexColor;
secondary_fixed: HexColor;
secondary_fixed_dim: HexColor;
shadow: HexColor;
surface: HexColor;
surface_bright: HexColor;
surface_container: HexColor;
surface_container_high: HexColor;
surface_container_highest: HexColor;
surface_container_low: HexColor;
surface_container_lowest: HexColor;
surface_dim: HexColor;
surface_variant: HexColor;
tertiary: HexColor;
tertiary_container: HexColor;
tertiary_fixed: HexColor;
tertiary_fixed_dim: HexColor;
};
export type MatugenVariation = {
rosewater: HexColor;
flamingo: HexColor;
pink: HexColor;
mauve: HexColor;
red: HexColor;
maroon: HexColor;
peach: HexColor;
yellow: HexColor;
green: HexColor;
teal: HexColor;
sky: HexColor;
sapphire: HexColor;
blue: HexColor;
lavender: HexColor;
text: HexColor;
subtext1: HexColor;
subtext2: HexColor;
overlay2: HexColor;
overlay1: HexColor;
overlay0: HexColor;
surface2: HexColor;
surface1: HexColor;
surface0: HexColor;
base2: HexColor;
base: HexColor;
mantle: HexColor;
crust: HexColor;
notifications_closer: HexColor;
notifications_background: HexColor;
dashboard_btn_text: HexColor;
red2: HexColor;
peach2: HexColor;
pink2: HexColor;
mantle2: HexColor;
surface1_2: HexColor;
surface0_2: HexColor;
overlay1_2: HexColor;
text2: HexColor;
lavender2: HexColor;
crust2: HexColor;
maroon2: HexColor;
mauve2: HexColor;
green2: HexColor;
surface2_2: HexColor;
sky2: HexColor;
teal2: HexColor;
yellow2: HexColor;
pink3: HexColor;
red3: HexColor;
mantle3: HexColor;
surface0_3: HexColor;
surface2_3: HexColor;
overlay1_3: HexColor;
lavender3: HexColor;
mauve3: HexColor;
green3: HexColor;
sky3: HexColor;
teal3: HexColor;
yellow3: HexColor;
maroon3: HexColor;
crust3: HexColor;
notifications_closer?: HexColor;
notifications_background?: HexColor;
dashboard_btn_text?: HexColor;
};
export type MatugenScheme =
| 'content'
| 'expressive'
| 'fidelity'
| 'fruit-salad'
| 'monochrome'
| 'neutral'
| 'rainbow'
| 'tonal-spot';
export type MatugenVariations =
| 'standard_1'
| 'standard_2'
| 'standard_3'
| 'monochrome_1'
| 'monochrome_2'
| 'monochrome_3'
| 'vivid_1'
| 'vivid_2'
| 'vivid_3';
type MatugenTheme = 'light' | 'dark';
export type ColorMapKey = keyof typeof defaultColorMap;
export type ColorMapValue = (typeof defaultColorMap)[ColorMapKey];
export type ScalingPriority = 'gdk' | 'hyprland' | 'both';
export type BluetoothBatteryState = 'paired' | 'connected' | 'always';
export type BorderLocation = 'none' | 'top' | 'right' | 'bottom' | 'left' | 'horizontal' | 'vertical' | 'full';
export type PositionAnchor = { [key: string]: Astal.WindowAnchor };
export type DropdownMenuList = (typeof dropdownMenuList)[number];

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

@@ -0,0 +1,52 @@
import { Widget } from 'types/widgets/widget';
import { WindowProps } from 'types/widgets/window';
import { Transition } from './widget';
import { Gtk } from 'astal/gtk3';
export type PopupWindowProps = {
name: string;
child?: BindableChild | BindableChild[];
layout?: Layouts;
transition?: Transition | Binding<Transition>;
exclusivity?: Exclusivity;
} & WindowProps;
export type LayoutFunction = (
name: string,
child: Widget,
transition: Gtk.RevealerTransitionType,
) => {
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';
export type Opts = {
className: string;
vexpand: boolean;
};
export type PaddingProps = {
name: string;
opts?: Opts;
};
export type PopupRevealerProps = {
name: string;
child: GtkWidget;
transition: Gtk.RevealerTransitionType;
};

1
src/lib/types/power.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export type Action = 'sleep' | 'reboot' | 'logout' | 'shutdown';

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

@@ -0,0 +1,9 @@
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;
};
export type ProfileType = 'balanced' | 'power-saver' | 'performance';

6
src/lib/types/systray.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export type SystrayIconMap = {
[key: string]: {
icon: string;
color: string;
};
};

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

@@ -0,0 +1,11 @@
import { EventArgs } from './eventArgs';
import { Variable } from 'types/variable';
export type ThrottleFn = (
cmd: string,
args: EventArgs,
fn?: (output: string) => void,
postInputUpdated?: Variable<boolean>,
) => void;
export type ThrottleFnCallback = ((output: string) => void) | undefined;

1
src/lib/types/variable.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export type Bind = OriginalBinding<GObject.Object, keyof Props<GObject.Object>, unknown>;

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

@@ -0,0 +1,3 @@
export type VolumeIcons = {
[index: number]: string;
};

118
src/lib/types/weather.d.ts vendored Normal file
View File

@@ -0,0 +1,118 @@
import { weatherIcons } from 'src/lib/icons/weather';
export type UnitType = 'imperial' | 'metric';
export type Weather = {
location: Location;
current: Current;
forecast: Forecast;
};
export type Current = {
last_updated_epoch?: number;
last_updated?: string;
temp_c: number;
temp_f: number;
is_day: number;
condition: Condition;
wind_mph: number;
wind_kph: number;
wind_degree: number;
wind_dir: string;
pressure_mb: number;
pressure_in: number;
precip_mm: number;
precip_in: number;
humidity: number;
cloud: number;
feelslike_c: number;
feelslike_f: number;
windchill_c: number;
windchill_f: number;
heatindex_c: number;
heatindex_f: number;
dewpoint_c: number;
dewpoint_f: number;
vis_km: number;
vis_miles: number;
uv: number;
gust_mph: number;
gust_kph: number;
time_epoch?: number;
time?: string;
snow_cm?: number;
will_it_rain?: number;
chance_of_rain?: number;
will_it_snow?: number;
chance_of_snow?: number;
};
export type Condition = {
text: string;
icon: string;
code: number;
};
export type Forecast = {
forecastday: Forecastday[];
};
export type Forecastday = {
date: string;
date_epoch: number;
day: Day;
astro: Astro;
hour: Current[];
};
export type Astro = {
sunrise: string;
sunset: string;
moonrise: string;
moonset: string;
moon_phase: string;
moon_illumination: number;
is_moon_up: number;
is_sun_up: number;
};
export type Day = {
maxtemp_c: number;
maxtemp_f: number;
mintemp_c: number;
mintemp_f: number;
avgtemp_c: number;
avgtemp_f: number;
maxwind_mph: number;
maxwind_kph: number;
totalprecip_mm: number;
totalprecip_in: number;
totalsnow_cm: number;
avgvis_km: number;
avgvis_miles: number;
avghumidity: number;
daily_will_it_rain: number;
daily_chance_of_rain: number;
daily_will_it_snow: number;
daily_chance_of_snow: number;
condition: Condition;
uv: number;
};
export type Location = {
name: string;
region: string;
country: string;
lat: number;
lon: number;
tz_id: string;
localtime_epoch: number;
localtime: string;
};
export type TemperatureIconColorMap = {
[key: number]: string;
};
export type WeatherIconTitle = keyof typeof weatherIcons;
export type WeatherIcon = (typeof weatherIcons)[WeatherIconTitle];

55
src/lib/types/widget.d.ts vendored Normal file
View File

@@ -0,0 +1,55 @@
import { Gdk, Gtk } from 'astal/gtk3';
import Box from 'types/widgets/box';
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';
export type Layouts =
| 'center'
| 'top'
| 'top-right'
| 'top-center'
| 'top-left'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
export type Attribute = unknown;
export type Child = Gtk.Widget;
export type GtkWidget = Gtk.Widget;
export type BoxWidget = Box<GtkWidget, Child>;
export type GdkEvent = Gdk.Event;
export type GButton = Gtk.Button;
export type GBox = Gtk.Box;
export type GLabel = Gtk.Label;
export type GCenterBox = Gtk.Box;
export type EventHandler<Self> = (self: Self, event: Gdk.Event) => boolean | unknown;
export type EventArgs = {
clicked: GtkWidget;
event: Gdk.EventButton | Gdk.EventScroll;
};
export interface WidgetProps {
onPrimaryClick?: (clicked: GtkWidget, event: Gdk.EventButton) => void;
onSecondaryClick?: (clicked: GtkWidget, event: Gdk.EventButton) => void;
onMiddleClick?: (clicked: GtkWidget, event: Gdk.EventButton) => void;
onScrollUp?: (clicked: GtkWidget, event: Gdk.EventScroll) => void;
onScrollDown?: (clicked: GtkWidget, event: Gdk.EventScroll) => void;
setup?: (self: GtkWidget) => void;
}
export interface GtkWidgetExtended extends Gtk.Widget {
props?: WidgetProps;
component?: JSX.Element;
primaryClick?: (clicked: GtkWidget, event: Astal.ClickEvent) => void;
isVisible?: boolean;
boxClass?: string;
isVis?: {
bind: (key: string) => Bind;
};
}
export type GtkWidget = GtkWidgetExtended;

36
src/lib/types/workspace.d.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
export type WorkspaceRule = {
workspaceString: string;
monitor: string;
};
export type WorkspaceMap = {
[key: string]: number[];
};
export type MonitorMap = {
[key: number]: string;
};
export type ApplicationIcons = {
[key: string]: string;
};
export type WorkspaceIcons = {
[key: string]: string;
};
export type AppIconOptions = {
iconMap: ApplicationIcons;
defaultIcon: string;
emptyIcon: string;
};
export type ClientAttributes = [className: string, title: string];
export type WorkspaceIconsColored = {
[key: string]: {
color: string;
icon: string;
};
};
export type WorkspaceIconMap = WorkspaceIcons | WorkspaceIconsColored;

414
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,414 @@
import { BarModule, NotificationAnchor, PositionAnchor } from './types/options';
import { OSDAnchor } from './types/options';
import icons, { substitutes } from './icons/icons';
import GLib from 'gi://GLib?version=2.0';
import GdkPixbuf from 'gi://GdkPixbuf';
import { NotificationArgs } from './types/notification';
import { namedColors } from './constants/colors';
import { distroIcons } from './constants/distro';
import { distro } from './variables';
import options from '../options';
import { Astal, Gdk, Gtk } from 'astal/gtk3';
import AstalApps from 'gi://AstalApps?version=0.1';
import { exec, execAsync } from 'astal/process';
import { Gio } from 'astal';
/**
* Handles errors by throwing a new Error with a message.
*
* This function takes an error object and throws a new Error with the provided message or a default message.
* If the error is an instance of Error, it uses the error's message. Otherwise, it converts the error to a string.
*
* @param error The error to handle.
*
* @throws Throws a new error with the provided message or a default message.
*/
export function errorHandler(error: unknown): never {
if (error instanceof Error) {
throw new Error(error.message);
}
throw new Error(String(error));
}
/**
* Looks up an icon by name and size.
*
* This function retrieves an icon from the default icon theme based on the provided name and size.
* If the name is not provided, it returns null.
*
* @param name The name of the icon to look up.
* @param size The size of the icon to look up. Defaults to 16.
*
* @returns The Gtk.IconInfo object if the icon is found, or null if not found.
*/
export function lookUpIcon(name?: string, size = 16): Gtk.IconInfo | null {
if (!name) return null;
return Gtk.IconTheme.get_default().lookup_icon(name, size, Gtk.IconLookupFlags.USE_BUILTIN);
}
/**
* Retrieves all unique layout items from the bar options.
*
* This function extracts all unique layout items from the bar options defined in the `options` object.
* It iterates through the layouts for each monitor and collects items from the left, middle, and right sections.
*
* @returns An array of unique layout items.
*/
export function getLayoutItems(): BarModule[] {
const { layouts } = options.bar;
const itemsInLayout: BarModule[] = [];
Object.keys(layouts.get()).forEach((monitor) => {
const leftItems = layouts.get()[monitor].left;
const rightItems = layouts.get()[monitor].right;
const middleItems = layouts.get()[monitor].middle;
itemsInLayout.push(...leftItems);
itemsInLayout.push(...middleItems);
itemsInLayout.push(...rightItems);
});
return [...new Set(itemsInLayout)];
}
/**
* Retrieves the appropriate icon based on the provided name and fallback.
*
* This function returns a substitute icon if available, the original name if it exists as a file, or a fallback icon.
* It also logs a message if no substitute icon is found.
*
* @param name The name of the icon to look up.
* @param fallback The fallback icon to use if the name is not found. Defaults to `icons.missing`.
*
* @returns The icon name or the fallback icon.
*/
export function icon(name: string | null, fallback = icons.missing): string {
const validateSubstitute = (name: string): name is keyof typeof substitutes => name in substitutes;
if (!name) return fallback || '';
if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name;
let icon: string = name;
if (validateSubstitute(name)) {
icon = substitutes[name];
}
if (lookUpIcon(icon)) return icon;
print(`no icon substitute "${icon}" for "${name}", fallback: "${fallback}"`);
return fallback;
}
/**
* Executes a bash command asynchronously.
*
* This function runs a bash command using `execAsync` and returns the output as a string.
* It handles errors by logging them and returning an empty string.
*
* @param strings The command to execute as a template string or a regular string.
* @param values Additional values to interpolate into the command.
*
* @returns A promise that resolves to the command output as a string.
*/
export async function bash(strings: TemplateStringsArray | string, ...values: unknown[]): Promise<string> {
const cmd =
typeof strings === 'string' ? strings : strings.flatMap((str, i) => str + `${values[i] ?? ''}`).join('');
return execAsync(['bash', '-c', cmd]).catch((err) => {
console.error(cmd, err);
return '';
});
}
/**
* Executes a shell command asynchronously.
*
* This function runs a shell command using `execAsync` and returns the output as a string.
* It handles errors by logging them and returning an empty string.
*
* @param cmd The command to execute as a string or an array of strings.
*
* @returns A promise that resolves to the command output as a string.
*/
export async function sh(cmd: string | string[]): Promise<string> {
return execAsync(cmd).catch((err) => {
console.error(typeof cmd === 'string' ? cmd : cmd.join(' '), err);
return '';
});
}
/**
* Generates an array of JSX elements for each monitor.
*
* This function creates an array of JSX elements by calling the provided widget function for each monitor.
* It uses the number of monitors available in the default Gdk display.
*
* @param widget A function that takes a monitor index and returns a JSX element.
*
* @returns An array of JSX elements, one for each monitor.
*/
export function forMonitors(widget: (monitor: number) => JSX.Element): JSX.Element[] {
const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
return range(n, 0).flatMap(widget);
}
/**
* Generates an array of numbers within a specified range.
*
* This function creates an array of numbers starting from the `start` value up to the specified `length`.
*
* @param length The length of the array to generate.
* @param start The starting value of the range. Defaults to 1.
*
* @returns An array of numbers within the specified range.
*/
export function range(length: number, start = 1): number[] {
return Array.from({ length }, (_, i) => i + start);
}
/**
* Checks if all specified dependencies are available.
*
* This function verifies the presence of the specified binaries using the `which` command.
* It logs a warning and sends a notification if any dependencies are missing.
*
* @param bins The list of binaries to check.
*
* @returns True if all dependencies are found, false otherwise.
*/
export function dependencies(...bins: string[]): boolean {
const missing = bins.filter((bin) => {
try {
exec(`which ${bin}`);
return false;
} catch (e) {
console.error(e);
return true;
}
});
if (missing.length > 0) {
console.warn(Error(`missing dependencies: ${missing.join(', ')}`));
Notify({
summary: 'Dependencies not found!',
body: `The following dependencies are missing: ${missing.join(', ')}`,
iconName: icons.ui.warning,
timeout: 7000,
});
}
return missing.length === 0;
}
/**
* Launches an application in a detached process.
*
* This function runs the specified application executable in the background using a bash command.
* It also increments the application's frequency counter.
*
* @param app The application to launch.
*/
export function launchApp(app: AstalApps.Application): void {
const exe = app.executable
.split(/\s+/)
.filter((str) => !str.startsWith('%') && !str.startsWith('@'))
.join(' ');
bash(`${exe} &`);
app.frequency += 1;
}
/**
* Checks if the provided filepath is a valid image.
*
* This function attempts to load an image from the specified filepath using GdkPixbuf.
* If the image is successfully loaded, it returns true. Otherwise, it logs an error and returns false.
*
* @param imgFilePath The path to the image file.
*
* @returns True if the filepath is a valid image, false otherwise.
*/
export function isAnImage(imgFilePath: string): boolean {
try {
const file = Gio.File.new_for_path(imgFilePath);
if (!file.query_exists(null)) {
return false;
}
GdkPixbuf.Pixbuf.new_from_file(imgFilePath);
return true;
} catch (error) {
console.error(error);
return false;
}
}
/**
* Sends a notification using the `notify-send` command.
*
* This function constructs a notification command based on the provided notification arguments and executes it asynchronously.
* It logs an error if the notification fails to send.
*
* @param notifPayload The notification arguments containing summary, body, appName, iconName, urgency, timeout, category, transient, and id.
*/
export function Notify(notifPayload: NotificationArgs): void {
let command = 'notify-send';
command += ` "${notifPayload.summary} "`;
if (notifPayload.body) command += ` "${notifPayload.body}" `;
if (notifPayload.appName) command += ` -a "${notifPayload.appName}"`;
if (notifPayload.iconName) command += ` -i "${notifPayload.iconName}"`;
if (notifPayload.urgency) command += ` -u "${notifPayload.urgency}"`;
if (notifPayload.timeout !== undefined) command += ` -t ${notifPayload.timeout}`;
if (notifPayload.category) command += ` -c "${notifPayload.category}"`;
if (notifPayload.transient) command += ` -e`;
if (notifPayload.id !== undefined) command += ` -r ${notifPayload.id}`;
execAsync(command)
.then()
.catch((err) => {
console.error(`Failed to send notification: ${err.message}`);
});
}
/**
* Maps a notification or OSD anchor position to an Astal window anchor.
*
* This function converts a position anchor from the notification or OSD settings to the corresponding Astal window anchor.
*
* @param pos The position anchor to convert.
*
* @returns The corresponding Astal window anchor.
*/
export function getPosition(pos: NotificationAnchor | OSDAnchor): Astal.WindowAnchor {
const positionMap: PositionAnchor = {
top: Astal.WindowAnchor.TOP,
'top right': Astal.WindowAnchor.TOP | Astal.WindowAnchor.RIGHT,
'top left': Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT,
bottom: Astal.WindowAnchor.BOTTOM,
'bottom right': Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.RIGHT,
'bottom left': Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT,
right: Astal.WindowAnchor.RIGHT,
left: Astal.WindowAnchor.LEFT,
};
return positionMap[pos] || Astal.WindowAnchor.TOP;
}
/**
* Validates if a string is a valid GJS color.
*
* This function checks if the provided string is a valid color in GJS.
* It supports named colors, hex colors, RGB, and RGBA formats.
*
* @param color The color string to validate.
*
* @returns True if the color is valid, false otherwise.
*/
export function isValidGjsColor(color: string): boolean {
const colorLower = color.toLowerCase().trim();
if (namedColors.has(colorLower)) {
return true;
}
const hexColorRegex = /^#(?:[a-fA-F0-9]{3,4}|[a-fA-F0-9]{6,8})$/;
const rgbRegex = /^rgb\(\s*(\d{1,3}%?\s*,\s*){2}\d{1,3}%?\s*\)$/;
const rgbaRegex = /^rgba\(\s*(\d{1,3}%?\s*,\s*){3}(0|1|0?\.\d+)\s*\)$/;
if (hexColorRegex.test(color)) {
return true;
}
if (rgbRegex.test(colorLower) || rgbaRegex.test(colorLower)) {
return true;
}
return false;
}
/**
* Capitalizes the first letter of a string.
*
* This function takes a string and returns a new string with the first letter capitalized.
*
* @param str The string to capitalize.
*
* @returns The input string with the first letter capitalized.
*/
export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/**
* Retrieves the icon for the current distribution.
*
* This function returns the icon associated with the current distribution based on the `distroIcons` array.
* If no icon is found, it returns a default icon.
*
* @returns The icon for the current distribution as a string.
*/
export function getDistroIcon(): string {
const icon = distroIcons.find(([id]) => id === distro.id);
return icon ? icon[1] : ''; // default icon if not found
}
/**
* Checks if an event is a primary click.
*
* This function determines if the provided event is a primary click based on the button property.
*
* @param event The click event to check.
*
* @returns True if the event is a primary click, false otherwise.
*/
export const isPrimaryClick = (event: Astal.ClickEvent): boolean => event.button === Gdk.BUTTON_PRIMARY;
/**
* Checks if an event is a secondary click.
*
* This function determines if the provided event is a secondary click based on the button property.
*
* @param event The click event to check.
*
* @returns True if the event is a secondary click, false otherwise.
*/
export const isSecondaryClick = (event: Astal.ClickEvent): boolean => event.button === Gdk.BUTTON_SECONDARY;
/**
* Checks if an event is a middle click.
*
* This function determines if the provided event is a middle click based on the button property.
*
* @param event The click event to check.
*
* @returns True if the event is a middle click, false otherwise.
*/
export const isMiddleClick = (event: Astal.ClickEvent): boolean => event.button === Gdk.BUTTON_MIDDLE;
/**
* Checks if an event is a scroll up.
*
* This function determines if the provided event is a scroll up based on the direction property.
*
* @param event The scroll event to check.
*
* @returns True if the event is a scroll up, false otherwise.
*/
export const isScrollUp = (event: Astal.ScrollEvent): boolean => event.direction === Gdk.ScrollDirection.UP;
/**
* Checks if an event is a scroll down.
*
* This function determines if the provided event is a scroll down based on the direction property.
*
* @param event The scroll event to check.
*
* @returns True if the event is a scroll down, false otherwise.
*/
export const isScrollDown = (event: Astal.ScrollEvent): boolean => event.direction === Gdk.ScrollDirection.DOWN;

18
src/lib/variables.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Variable } from 'astal';
import GLib from 'gi://GLib';
export const clock = Variable(GLib.DateTime.new_now_local()).poll(
1000,
(): GLib.DateTime => GLib.DateTime.new_now_local(),
);
export const uptime = Variable(0).poll(
60_00,
'cat /proc/uptime',
(line): number => Number.parseInt(line.split('.')[0]) / 60,
);
export const distro = {
id: GLib.get_os_info('ID'),
logo: GLib.get_os_info('LOGO'),
};