Minor: Refactor the code-base for better organization and compartmentalization. (#934)
* Clean up unused code * Fix media player formatting issue for labels with new line characteres. * Refactor the media player handlers into a class. * More code cleanup and organize shared weather utils into distinct classes. * Flatten some nesting. * Move weather manager in dedicated class and build HTTP Utility class for Rest API calling. * Remove logs * Rebase master merge * Reorg code (WIP) * More reorg * Delete utility scripts * Reorg options * Finish moving all options over * Fix typescript issues * Update options imports to default * missed update * Screw barrel files honestly, work of the devil. * Only initialize power profiles if power-profiles-daemon is running. * Fix window positioning and weather service naming * style dir * More organization * Restructure types to be closer to their source * Remove lib types and constants * Update basic weather object to be saner with extensibility. * Service updates * Fix initialization strategy for services. * Fix Config Manager to only emit changed objects and added missing temp converters. * Update storage service to handle unit changes. * Added cpu temp sensor auto-discovery * Added missing JSDocs to services * remove unused * Migrate to network service. * Fix network password issue. * Move out password input into helper * Rename password mask constant to be less double-negativey. * Dropdown menu rename * Added a component to edit JSON in the settings dialog (rough/WIP) * Align settings * Add and style JSON Editor. * Adjust padding * perf(shortcuts): ⚡ avoid unnecessary polling when shortcuts are disabled Stops the recording poller when shortcuts are disabled, preventing redundant polling and reducing resource usage. * Fix types and return value if shortcut not enabled. * Move the swww daemon checking process outside of the wallpaper service into a dedicated deamon lifecyle processor. * Add more string formatters and use title case for weather status (as it was). * Fix startup errors. * Rgba fix * Remove zod from dependencies --------- Co-authored-by: KernelDiego <gonzalezdiego.contact@gmail.com>
This commit is contained in:
18
src/lib/array/helpers.ts
Normal file
18
src/lib/array/helpers.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Generates an array of numbers within a specified range
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicate values from an array
|
||||
* @param array - The array to deduplicate
|
||||
* @returns A new array with unique values
|
||||
*/
|
||||
export function unique<T>(array: T[]): T[] {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
40
src/lib/bar/helpers.ts
Normal file
40
src/lib/bar/helpers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import options from 'src/configuration';
|
||||
import { BarLayouts, BarModule } from '../options/types';
|
||||
import { unique } from '../array/helpers';
|
||||
|
||||
/**
|
||||
* 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 unique(itemsInLayout);
|
||||
}
|
||||
|
||||
export function setLayout(layout: BarLayouts): string {
|
||||
try {
|
||||
const { layouts } = options.bar;
|
||||
|
||||
layouts.set(layout);
|
||||
return 'Successfully updated layout.';
|
||||
} catch (error) {
|
||||
return `Failed to set layout: ${error}`;
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { App } from 'astal/gtk3';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { BarVisibility } from 'src/cli/utils/BarVisibility';
|
||||
import { forceUpdater } from 'src/components/bar/modules/workspaces/helpers';
|
||||
import options from 'src/options';
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const { autoHide } = options.bar;
|
||||
|
||||
/**
|
||||
* Sets bar visibility for a specific monitor
|
||||
*
|
||||
* @param monitorId - The ID of the monitor
|
||||
* @param isVisible - Whether the bar should be visible
|
||||
*/
|
||||
function setBarVisibility(monitorId: number, isVisible: boolean): void {
|
||||
const barName = `bar-${monitorId}`;
|
||||
|
||||
if (BarVisibility.get(barName)) {
|
||||
App.get_window(barName)?.set_visible(isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles bar visibility when a client's fullscreen state changes
|
||||
*
|
||||
* @param client - The Hyprland client that gained focus
|
||||
*/
|
||||
function handleFullscreenClientVisibility(client: AstalHyprland.Client): void {
|
||||
if (client === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fullscreenBinding = bind(client, 'fullscreen');
|
||||
|
||||
Variable.derive([bind(fullscreenBinding)], (isFullScreen) => {
|
||||
if (autoHide.get() === 'fullscreen') {
|
||||
setBarVisibility(client.monitor.id, !Boolean(isFullScreen));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows bars on all monitors
|
||||
*/
|
||||
function showAllBars(): void {
|
||||
const monitors = hyprlandService.get_monitors();
|
||||
|
||||
monitors.forEach((monitor) => {
|
||||
if (BarVisibility.get(`bar-${monitor.id}`)) {
|
||||
setBarVisibility(monitor.id, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates bar visibility based on workspace window count
|
||||
*/
|
||||
function updateBarVisibilityByWindowCount(): void {
|
||||
const monitors = hyprlandService.get_monitors();
|
||||
const activeWorkspaces = monitors.map((monitor) => monitor.active_workspace);
|
||||
|
||||
activeWorkspaces.forEach((workspace) => {
|
||||
const hasOneClient = workspace.get_clients().length !== 1;
|
||||
setBarVisibility(workspace.monitor.id, hasOneClient);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates bar visibility based on workspace fullscreen state
|
||||
*/
|
||||
function updateBarVisibilityByFullscreen(): void {
|
||||
hyprlandService.get_workspaces().forEach((workspace) => {
|
||||
setBarVisibility(workspace.monitor.id, !workspace.hasFullscreen);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the auto-hide behavior for bars
|
||||
* Manages visibility based on window count, fullscreen state, and user preferences
|
||||
*/
|
||||
export function initializeAutoHide(): void {
|
||||
Variable.derive(
|
||||
[
|
||||
bind(autoHide),
|
||||
bind(hyprlandService, 'workspaces'),
|
||||
bind(forceUpdater),
|
||||
bind(hyprlandService, 'focusedWorkspace'),
|
||||
],
|
||||
(hideMode) => {
|
||||
if (hideMode === 'never') {
|
||||
showAllBars();
|
||||
} else if (hideMode === 'single-window') {
|
||||
updateBarVisibilityByWindowCount();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Variable.derive([bind(hyprlandService, 'focusedClient')], (currentClient) => {
|
||||
handleFullscreenClientVisibility(currentClient);
|
||||
});
|
||||
|
||||
Variable.derive([bind(autoHide)], (hideMode) => {
|
||||
if (hideMode === 'fullscreen') {
|
||||
updateBarVisibilityByFullscreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import AstalBattery from 'gi://AstalBattery?version=0.1';
|
||||
import icons from '../icons/icons';
|
||||
import { Notify } from '../utils';
|
||||
import options from 'src/options';
|
||||
|
||||
const batteryService = AstalBattery.get_default();
|
||||
const {
|
||||
lowBatteryThreshold,
|
||||
lowBatteryNotification,
|
||||
lowBatteryNotificationText,
|
||||
lowBatteryNotificationTitle,
|
||||
} = options.menus.power;
|
||||
|
||||
export function warnOnLowBattery(): void {
|
||||
let sentLowNotification = false;
|
||||
let sentHalfLowNotification = false;
|
||||
|
||||
batteryService.connect('notify::charging', () => {
|
||||
if (batteryService.charging) {
|
||||
sentLowNotification = false;
|
||||
sentHalfLowNotification = false;
|
||||
}
|
||||
});
|
||||
|
||||
batteryService.connect('notify::percentage', () => {
|
||||
if (lowBatteryNotification.get() === undefined || batteryService.charging) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batteryPercentage = Math.floor(batteryService.percentage * 100);
|
||||
const lowThreshold = lowBatteryThreshold.get();
|
||||
|
||||
// Avoid double notification
|
||||
let sendNotification = false;
|
||||
|
||||
if (!sentLowNotification && batteryPercentage <= lowThreshold) {
|
||||
sentLowNotification = true;
|
||||
sendNotification = true;
|
||||
}
|
||||
|
||||
if (!sentHalfLowNotification && batteryPercentage <= lowThreshold / 2) {
|
||||
sentHalfLowNotification = true;
|
||||
sendNotification = true;
|
||||
}
|
||||
|
||||
if (sendNotification) {
|
||||
Notify({
|
||||
summary: lowBatteryNotificationTitle
|
||||
.get()
|
||||
.replaceAll('$POWER_LEVEL', batteryPercentage.toString()),
|
||||
body: lowBatteryNotificationText
|
||||
.get()
|
||||
.replaceAll('$POWER_LEVEL', batteryPercentage.toString()),
|
||||
iconName: icons.ui.warning,
|
||||
urgency: 'critical',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
|
||||
const floatSettingsDialog = (): void => {
|
||||
hyprlandService.message('keyword windowrulev2 float, title:^(hyprpanel-settings)$');
|
||||
|
||||
hyprlandService.connect('config-reloaded', () => {
|
||||
hyprlandService.message('keyword windowrulev2 float, title:^(hyprpanel-settings)$');
|
||||
});
|
||||
};
|
||||
|
||||
const floatFilePicker = (): void => {
|
||||
hyprlandService.message('keyword windowrulev2 float, title:^((Save|Import) Hyprpanel.*)$');
|
||||
|
||||
hyprlandService.connect('config-reloaded', () => {
|
||||
hyprlandService.message('keyword windowrulev2 float, title:^((Save|Import) Hyprpanel.*)$');
|
||||
});
|
||||
};
|
||||
|
||||
export const hyprlandSettings = (): void => {
|
||||
floatSettingsDialog();
|
||||
floatFilePicker();
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import './autoHide';
|
||||
import { initializeAutoHide } from './autoHide';
|
||||
import { warnOnLowBattery } from './batteryWarning';
|
||||
import { hyprlandSettings } from './hyprlandRules';
|
||||
|
||||
export const initializeSystemBehaviors = (): void => {
|
||||
warnOnLowBattery();
|
||||
initializeAutoHide();
|
||||
hyprlandSettings();
|
||||
};
|
||||
@@ -1,139 +0,0 @@
|
||||
export const defaultWindowTitleMap = [
|
||||
// Misc
|
||||
['kitty', '', 'Kitty Terminal'],
|
||||
['firefox', '', 'Firefox'],
|
||||
['microsoft-edge', '', 'Edge'],
|
||||
['discord', '', 'Discord'],
|
||||
['vesktop', '', 'Vesktop'],
|
||||
['org.kde.dolphin', '', 'Dolphin'],
|
||||
['plex', '', 'Plex'],
|
||||
['steam', '', 'Steam'],
|
||||
['spotify', '', 'Spotify'],
|
||||
['ristretto', '', 'Ristretto'],
|
||||
['obsidian', '', 'Obsidian'],
|
||||
['rofi', '', 'Rofi'],
|
||||
['qBittorrent$', '', 'QBittorrent'],
|
||||
|
||||
// Browsers
|
||||
['google-chrome', '', 'Google Chrome'],
|
||||
['brave-browser', '', 'Brave Browser'],
|
||||
['chromium', '', 'Chromium'],
|
||||
['opera', '', 'Opera'],
|
||||
['vivaldi', '', 'Vivaldi'],
|
||||
['waterfox', '', 'Waterfox'],
|
||||
['thorium', '', 'Thorium'],
|
||||
['tor-browser', '', 'Tor Browser'],
|
||||
['floorp', '', 'Floorp'],
|
||||
['zen', '', 'Zen Browser'],
|
||||
|
||||
// Terminals
|
||||
['gnome-terminal', '', 'GNOME Terminal'],
|
||||
['konsole', '', 'Konsole'],
|
||||
['alacritty', '', 'Alacritty'],
|
||||
['wezterm', '', 'Wezterm'],
|
||||
['foot', '', 'Foot Terminal'],
|
||||
['tilix', '', 'Tilix'],
|
||||
['xterm', '', 'XTerm'],
|
||||
['urxvt', '', 'URxvt'],
|
||||
['com.mitchellh.ghostty', '', 'Ghostty'],
|
||||
['^st$', '', 'st Terminal'],
|
||||
|
||||
// Development Tools
|
||||
['code', '', 'Visual Studio Code'],
|
||||
['vscode', '', 'VS Code'],
|
||||
['sublime-text', '', 'Sublime Text'],
|
||||
['atom', '', 'Atom'],
|
||||
['android-studio', '', 'Android Studio'],
|
||||
['jetbrains-idea', '', 'IntelliJ IDEA'],
|
||||
['jetbrains-pycharm', '', 'PyCharm'],
|
||||
['jetbrains-webstorm', '', 'WebStorm'],
|
||||
['jetbrains-phpstorm', '', 'PhpStorm'],
|
||||
['eclipse', '', 'Eclipse'],
|
||||
['netbeans', '', 'NetBeans'],
|
||||
['docker', '', 'Docker'],
|
||||
['vim', '', 'Vim'],
|
||||
['neovim', '', 'Neovim'],
|
||||
['neovide', '', 'Neovide'],
|
||||
['emacs', '', 'Emacs'],
|
||||
|
||||
// Communication Tools
|
||||
['slack', '', 'Slack'],
|
||||
['telegram-desktop', '', 'Telegram'],
|
||||
['org.telegram.desktop', '', 'Telegram'],
|
||||
['whatsapp', '', 'WhatsApp'],
|
||||
['teams', '', 'Microsoft Teams'],
|
||||
['skype', '', 'Skype'],
|
||||
['thunderbird', '', 'Thunderbird'],
|
||||
|
||||
// File Managers
|
||||
['nautilus', '', 'Files (Nautilus)'],
|
||||
['thunar', '', 'Thunar'],
|
||||
['pcmanfm', '', 'PCManFM'],
|
||||
['nemo', '', 'Nemo'],
|
||||
['ranger', '', 'Ranger'],
|
||||
['doublecmd', '', 'Double Commander'],
|
||||
['krusader', '', 'Krusader'],
|
||||
|
||||
// Media Players
|
||||
['vlc', '', 'VLC Media Player'],
|
||||
['mpv', '', 'MPV'],
|
||||
['rhythmbox', '', 'Rhythmbox'],
|
||||
|
||||
// Graphics Tools
|
||||
['gimp', '', 'GIMP'],
|
||||
['inkscape', '', 'Inkscape'],
|
||||
['krita', '', 'Krita'],
|
||||
['blender', '', 'Blender'],
|
||||
|
||||
// Video Editing
|
||||
['kdenlive', '', 'Kdenlive'],
|
||||
|
||||
// Games and Gaming Platforms
|
||||
['lutris', '', 'Lutris'],
|
||||
['heroic', '', 'Heroic Games Launcher'],
|
||||
['minecraft', '', 'Minecraft'],
|
||||
['csgo', '', 'CS:GO'],
|
||||
['dota2', '', 'Dota 2'],
|
||||
|
||||
// Office and Productivity
|
||||
['evernote', '', 'Evernote'],
|
||||
['sioyek', '', 'Sioyek'],
|
||||
|
||||
// Cloud Services and Sync
|
||||
['dropbox', '', 'Dropbox'],
|
||||
];
|
||||
|
||||
const overrides = {
|
||||
kitty: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a mapping of application names to their corresponding icons.
|
||||
* Uses the defaultWindowTitleMap to create the base mapping and applies any overrides.
|
||||
*
|
||||
* @returns An object where keys are application names and values are icon names.
|
||||
* If an application name exists in the overrides, that value is used instead of the default.
|
||||
*
|
||||
* @example
|
||||
* // Given:
|
||||
* defaultWindowTitleMap = [['kitty', '', 'Kitty Terminal'], ['firefox', '', 'Firefox']]
|
||||
* overrides = { 'kitty': '' }
|
||||
*
|
||||
* // Returns:
|
||||
* { 'kitty': '', 'firefox': '' }
|
||||
*/
|
||||
export const defaultApplicationIconMap = defaultWindowTitleMap.reduce(
|
||||
(iconMapAccumulator: Record<string, string>, windowTitles) => {
|
||||
const currentIconMap = iconMapAccumulator;
|
||||
|
||||
const appName: string = windowTitles[0];
|
||||
const appIcon: string = windowTitles[1];
|
||||
|
||||
if (!(appName in currentIconMap)) {
|
||||
currentIconMap[appName] = appIcon;
|
||||
}
|
||||
|
||||
return currentIconMap;
|
||||
},
|
||||
overrides,
|
||||
);
|
||||
@@ -1,31 +0,0 @@
|
||||
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', ''],
|
||||
];
|
||||
@@ -1,22 +0,0 @@
|
||||
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',
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { DropdownMenuList } from '../options/options.types';
|
||||
|
||||
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);
|
||||
};
|
||||
8
src/lib/events/dropdown.ts
Normal file
8
src/lib/events/dropdown.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { EventBox } from 'astal/gtk3/widget';
|
||||
import Variable from 'astal/variable';
|
||||
|
||||
type GlobalEventBoxes = {
|
||||
[key: string]: EventBox;
|
||||
};
|
||||
|
||||
export const globalEventBoxes: Variable<GlobalEventBoxes> = Variable({});
|
||||
62
src/lib/events/mouse.ts
Normal file
62
src/lib/events/mouse.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Astal, Gdk } from 'astal/gtk3';
|
||||
|
||||
/**
|
||||
* Checks if an event is a primary click
|
||||
* @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
|
||||
* @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
|
||||
* @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
|
||||
* @param event - The scroll event to check
|
||||
* @returns True if the event is a scroll up, false otherwise
|
||||
*/
|
||||
export const isScrollUp = (event: Gdk.Event): boolean => {
|
||||
const [directionSuccess, direction] = event.get_scroll_direction();
|
||||
const [deltaSuccess, , yScroll] = event.get_scroll_deltas();
|
||||
|
||||
if (directionSuccess && direction === Gdk.ScrollDirection.UP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deltaSuccess && yScroll < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if an event is a scroll down
|
||||
* @param event - The scroll event to check
|
||||
* @returns True if the event is a scroll down, false otherwise
|
||||
*/
|
||||
export const isScrollDown = (event: Gdk.Event): boolean => {
|
||||
const [directionSuccess, direction] = event.get_scroll_direction();
|
||||
const [deltaSuccess, , yScroll] = event.get_scroll_deltas();
|
||||
|
||||
if (directionSuccess && direction === Gdk.ScrollDirection.DOWN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deltaSuccess && yScroll > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
27
src/lib/httpClient/HttpError.ts
Normal file
27
src/lib/httpClient/HttpError.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { HttpErrorOptions } from './types';
|
||||
|
||||
/**
|
||||
* Custom error class for HTTP request failures
|
||||
* Provides status code and response data for error handling
|
||||
*/
|
||||
export class HttpError extends Error {
|
||||
public status: number;
|
||||
public data?: unknown;
|
||||
public url?: string;
|
||||
public method?: string;
|
||||
|
||||
constructor(options: HttpErrorOptions) {
|
||||
const { status, message, data, url, method } = options;
|
||||
|
||||
const errorMessage = message ? `: ${message}` : '';
|
||||
const response = `HTTP ${status}${errorMessage}`;
|
||||
|
||||
super(response);
|
||||
this.name = 'HttpError';
|
||||
|
||||
this.status = status;
|
||||
this.data = data;
|
||||
this.url = url;
|
||||
this.method = method;
|
||||
}
|
||||
}
|
||||
268
src/lib/httpClient/index.ts
Normal file
268
src/lib/httpClient/index.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { GLib } from 'astal';
|
||||
import Soup from 'gi://Soup?version=3.0';
|
||||
import { HttpError } from './HttpError';
|
||||
import { RequestOptions, RestResponse } from './types';
|
||||
import { errorHandler } from 'src/core/errors/handler';
|
||||
|
||||
/**
|
||||
* HTTP client wrapper for Soup.Session providing a Promise-based API
|
||||
* Handles authentication, timeouts, and JSON parsing automatically
|
||||
*/
|
||||
class HttpClient {
|
||||
private _session: Soup.Session;
|
||||
constructor(defaultTimeout = 30) {
|
||||
this._session = new Soup.Session();
|
||||
this._session.timeout = defaultTimeout;
|
||||
this._session.user_agent = 'HyprPanel/1.0';
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* HTTP Methods *
|
||||
*******************************************/
|
||||
|
||||
/**
|
||||
* Performs an HTTP GET request
|
||||
* @param url - Target URL for the request
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async get(url: string, options?: RequestOptions): Promise<RestResponse> {
|
||||
return this._request('GET', url, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP POST request
|
||||
* @param url - Target URL for the request
|
||||
* @param data - Request payload to send
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async post(
|
||||
url: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: RequestOptions,
|
||||
): Promise<RestResponse> {
|
||||
return this._request('POST', url, { ...options, body: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP PUT request
|
||||
* @param url - Target URL for the request
|
||||
* @param data - Request payload to send
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async put(
|
||||
url: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: RequestOptions,
|
||||
): Promise<RestResponse> {
|
||||
return this._request('PUT', url, { ...options, body: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP PATCH request
|
||||
* @param url - Target URL for the request
|
||||
* @param data - Request payload with partial updates
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async patch(
|
||||
url: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: RequestOptions,
|
||||
): Promise<RestResponse> {
|
||||
return this._request('PATCH', url, { ...options, body: data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP DELETE request
|
||||
* @param url - Target URL for the request
|
||||
* @param options - Optional configuration for the request
|
||||
*/
|
||||
public async delete(url: string, options?: RequestOptions): Promise<RestResponse> {
|
||||
return this._request('DELETE', url, options);
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* SOUP Infrastructure *
|
||||
*******************************************/
|
||||
|
||||
/**
|
||||
* Internal request handler for all HTTP methods
|
||||
* @param method - HTTP method to use
|
||||
* @param url - Target URL for the request
|
||||
* @param options - Configuration options for the request
|
||||
* @private
|
||||
*/
|
||||
private async _request(method: string, url: string, options: RequestOptions = {}): Promise<RestResponse> {
|
||||
const requestPromise = new Promise<RestResponse>((resolve, reject) => {
|
||||
const message = Soup.Message.new(method, url);
|
||||
|
||||
if (!message) {
|
||||
return reject(new Error(`Failed to create request for ${url}`));
|
||||
}
|
||||
|
||||
this._assignHeaders(message, options);
|
||||
this._constructBodyIfExists(method, options, message);
|
||||
|
||||
if (options.timeout) {
|
||||
this._session.timeout = options.timeout / 1000;
|
||||
}
|
||||
|
||||
this._sendRequest(resolve, reject, message, options);
|
||||
});
|
||||
|
||||
return requestPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs and sets the request body for HTTP methods that support it
|
||||
* @param method - HTTP method being used
|
||||
* @param options - Request options containing the body
|
||||
* @param message - Soup message to attach the body to
|
||||
*/
|
||||
private _constructBodyIfExists(method: string, options: RequestOptions, message: Soup.Message): void {
|
||||
const canContainBody = ['POST', 'PUT', 'PATCH'].includes(method);
|
||||
if (options.body && canContainBody) {
|
||||
let body: string;
|
||||
let contentType = options.headers?.['Content-Type'] || 'application/json';
|
||||
|
||||
if (typeof options.body === 'object') {
|
||||
body = JSON.stringify(options.body);
|
||||
} else {
|
||||
body = options.body;
|
||||
contentType = contentType || 'text/plain';
|
||||
}
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
const bytes = new GLib.Bytes(textEncoder.encode(body));
|
||||
message.set_request_body_from_bytes(contentType, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns headers to the request message
|
||||
* @param message - Soup message to add headers to
|
||||
* @param options - Request options containing headers
|
||||
*/
|
||||
private _assignHeaders(message: Soup.Message, options: RequestOptions): Soup.MessageHeaders {
|
||||
const headers = message.get_request_headers();
|
||||
|
||||
if (options.headers) {
|
||||
Object.entries(options.headers).forEach(([key, value]) => {
|
||||
headers.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the HTTP request and handles the response
|
||||
* @param resolve - Promise resolve callback
|
||||
* @param reject - Promise reject callback
|
||||
* @param message - Prepared Soup message to send
|
||||
* @param options - Request configuration options
|
||||
*/
|
||||
private _sendRequest(
|
||||
resolve: (value: RestResponse | PromiseLike<RestResponse>) => void,
|
||||
reject: (reason?: unknown) => void,
|
||||
message: Soup.Message,
|
||||
options: RequestOptions,
|
||||
): void {
|
||||
const cancellable = options.signal ?? null;
|
||||
|
||||
try {
|
||||
const bytes = this._session.send_and_read(message, cancellable);
|
||||
|
||||
const {
|
||||
response: responseText,
|
||||
headers: responseHeaders,
|
||||
status,
|
||||
} = this._decodeResponseSync(message, bytes);
|
||||
|
||||
const responseData = this._parseReponseData(options, responseText);
|
||||
|
||||
const response: RestResponse = {
|
||||
data: responseData,
|
||||
status,
|
||||
headers: responseHeaders,
|
||||
};
|
||||
|
||||
if (status >= 400) {
|
||||
const httpError = new HttpError({
|
||||
status,
|
||||
data: responseData,
|
||||
url: message.get_uri().to_string(),
|
||||
method: message.get_method(),
|
||||
});
|
||||
|
||||
return reject(httpError);
|
||||
}
|
||||
|
||||
return resolve(response);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the response bytes into text and extracts response metadata
|
||||
* @param message - Soup message containing the response
|
||||
* @param bytes - Response bytes from the sync request
|
||||
*/
|
||||
private _decodeResponseSync(
|
||||
message: Soup.Message,
|
||||
bytes: GLib.Bytes | null,
|
||||
): {
|
||||
response: string;
|
||||
status: Soup.Status;
|
||||
headers: Record<string, string>;
|
||||
} {
|
||||
if (!bytes) {
|
||||
throw new Error('No response received');
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const byteData = bytes.get_data();
|
||||
|
||||
const responseText = byteData ? decoder.decode(byteData) : '';
|
||||
const status = message.get_status();
|
||||
|
||||
const responseHeaders: Record<string, string> = {};
|
||||
|
||||
message.get_response_headers().foreach((name, value) => {
|
||||
responseHeaders[name] = value;
|
||||
});
|
||||
|
||||
return {
|
||||
response: responseText,
|
||||
status,
|
||||
headers: responseHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses response text based on the expected response type
|
||||
* @param options - Request options containing responseType preference
|
||||
* @param responseText - Raw response text to parse
|
||||
*/
|
||||
private _parseReponseData(
|
||||
options: RequestOptions,
|
||||
responseText: string,
|
||||
): string | Record<string, unknown> {
|
||||
if (!responseText) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (options.responseType === 'text') {
|
||||
return responseText;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedResponseText = JSON.parse(responseText);
|
||||
return parsedResponseText;
|
||||
} catch (e) {
|
||||
errorHandler(`Failed to parse JSON response: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const httpClient = new HttpClient();
|
||||
23
src/lib/httpClient/types.ts
Normal file
23
src/lib/httpClient/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Gio } from 'astal';
|
||||
|
||||
export interface RequestOptions {
|
||||
headers?: Record<string, string>;
|
||||
body?: string | object;
|
||||
timeout?: number;
|
||||
responseType?: 'json' | 'text';
|
||||
signal?: Gio.Cancellable;
|
||||
}
|
||||
|
||||
export interface RestResponse {
|
||||
data: unknown;
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface HttpErrorOptions {
|
||||
status: number;
|
||||
message?: string;
|
||||
data?: unknown;
|
||||
url?: string;
|
||||
method?: string;
|
||||
}
|
||||
32
src/lib/icons/helpers.ts
Normal file
32
src/lib/icons/helpers.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
|
||||
/**
|
||||
* Looks up an icon by name and size
|
||||
* @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 === undefined) return null;
|
||||
|
||||
return Gtk.IconTheme.get_default().lookup_icon(name, size, Gtk.IconLookupFlags.USE_BUILTIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an icon exists in the theme
|
||||
* @param name - The name of the icon to check
|
||||
* @returns True if the icon exists, false otherwise
|
||||
*/
|
||||
export function iconExists(name: string): boolean {
|
||||
return lookUpIcon(name) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an icon name with fallback
|
||||
* @param primary - The primary icon name to try
|
||||
* @param fallback - The fallback icon name if primary doesn't exist
|
||||
* @returns The primary icon if it exists, otherwise the fallback
|
||||
*/
|
||||
export function getIconWithFallback(primary: string, fallback: string): string {
|
||||
return iconExists(primary) ? primary : fallback;
|
||||
}
|
||||
@@ -1,17 +1,3 @@
|
||||
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: {
|
||||
@@ -141,4 +127,59 @@ export default {
|
||||
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',
|
||||
moderate_or_heavy_rain_in_area_with_thunder: 'weather-showers-symbolic',
|
||||
patchy_light_snow_with_thunder: 'weather-snow-symbolic',
|
||||
moderate_or_heavy_snow_with_thunder: 'weather-snow-symbolic',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
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',
|
||||
moderate_or_heavy_rain_in_area_with_thunder: 'weather-showers-symbolic',
|
||||
patchy_light_snow_with_thunder: 'weather-snow-symbolic',
|
||||
moderate_or_heavy_snow_with_thunder: 'weather-snow-symbolic',
|
||||
},
|
||||
} as const;
|
||||
@@ -1,55 +0,0 @@
|
||||
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: '',
|
||||
moderate_or_heavy_rain_in_area_with_thunder: '',
|
||||
patchy_light_snow_with_thunder: '',
|
||||
moderate_or_heavy_snow_with_thunder: '',
|
||||
} as const;
|
||||
@@ -1,169 +0,0 @@
|
||||
import { readFile, writeFile, monitorFile } from 'astal/file';
|
||||
import { errorHandler, Notify } from '../utils';
|
||||
import { ensureDirectory } from '../session';
|
||||
import icons from '../icons/icons';
|
||||
|
||||
/**
|
||||
* Manages configuration file operations including reading, writing, and change monitoring
|
||||
*
|
||||
* The ConfigManager centralizes all configuration persistence operations and provides
|
||||
* utilities for working with nested configuration structures.
|
||||
*/
|
||||
export class ConfigManager {
|
||||
private _configPath: string;
|
||||
private _changeCallbacks: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* Creates a new configuration manager for a specific config file
|
||||
*
|
||||
* @param configPath - Path to the configuration file to manage
|
||||
*/
|
||||
constructor(configPath: string) {
|
||||
this._configPath = configPath;
|
||||
this._ensureConfigDirectory();
|
||||
this._setupConfigMonitor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single option in the configuration file
|
||||
*
|
||||
* @param id - Dot-notation path of the option to update
|
||||
* @param value - New value to store for the option
|
||||
*/
|
||||
public updateOption(id: string, value: unknown): void {
|
||||
const config = this.readConfig();
|
||||
config[id] = value;
|
||||
this.writeConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a value from a nested object using a path
|
||||
*
|
||||
* @param dataObject - The object to search within
|
||||
* @param path - Dot-notation path or array of path segments
|
||||
* @returns The value at the specified path or undefined if not found
|
||||
*/
|
||||
public getNestedValue(dataObject: Record<string, unknown>, path: string | string[]): unknown {
|
||||
const pathArray = typeof path === 'string' ? path.split('.') : path;
|
||||
return this._findValueByPath(dataObject, pathArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current configuration from disk
|
||||
*
|
||||
* @returns The parsed configuration object or an empty object if the file doesn't exist
|
||||
*/
|
||||
public readConfig(): Record<string, unknown> {
|
||||
const raw = readFile(this._configPath);
|
||||
if (!raw || raw.trim() === '') {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
this._handleConfigError(error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes configuration to disk
|
||||
*
|
||||
* @param config - The configuration object to serialize and save
|
||||
*/
|
||||
public writeConfig(config: Record<string, unknown>): void {
|
||||
writeFile(this._configPath, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be called when the config file changes
|
||||
*
|
||||
* @param callback - Function to execute when config file changes are detected
|
||||
*/
|
||||
public onConfigChanged(callback: () => void): void {
|
||||
this._changeCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively navigates an object to find a value at the specified path
|
||||
*
|
||||
* @param currentObject - The object currently being traversed
|
||||
* @param pathKeys - Remaining path segments to navigate
|
||||
* @returns The value at the path or undefined if not found
|
||||
*/
|
||||
private _findValueByPath(currentObject: Record<string, unknown>, pathKeys: string[]): unknown {
|
||||
const currentKey = pathKeys.shift();
|
||||
if (currentKey === undefined) {
|
||||
return currentObject;
|
||||
}
|
||||
if (!this._isObject(currentObject)) {
|
||||
return;
|
||||
}
|
||||
const propertyPath = [currentKey, ...pathKeys].join('.');
|
||||
if (propertyPath in currentObject) {
|
||||
return currentObject[propertyPath];
|
||||
}
|
||||
if (!(currentKey in currentObject)) {
|
||||
return;
|
||||
}
|
||||
const currentKeyValue = currentObject[currentKey];
|
||||
if (!this._isObject(currentKeyValue)) {
|
||||
return;
|
||||
}
|
||||
return this._findValueByPath(currentKeyValue, pathKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the directory for the config file exists
|
||||
*/
|
||||
private _ensureConfigDirectory(): void {
|
||||
ensureDirectory(this._configPath.split('/').slice(0, -1).join('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up file monitoring to detect external changes to the config file
|
||||
*/
|
||||
private _setupConfigMonitor(): void {
|
||||
const debounceTimeMs = 200;
|
||||
let lastEventTime = Date.now();
|
||||
monitorFile(this._configPath, () => {
|
||||
if (Date.now() - lastEventTime < debounceTimeMs) {
|
||||
return;
|
||||
}
|
||||
lastEventTime = Date.now();
|
||||
this._notifyConfigChanged();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all registered callbacks about config file changes
|
||||
*/
|
||||
private _notifyConfigChanged(): void {
|
||||
this._changeCallbacks.forEach((callback) => callback());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles configuration parsing errors with appropriate logging and notification
|
||||
*
|
||||
* @param error - The error that occurred during config parsing
|
||||
*/
|
||||
private _handleConfigError(error: unknown): void {
|
||||
console.error(`Failed to load config file: ${error}`);
|
||||
Notify({
|
||||
summary: 'Failed to load config file',
|
||||
body: `${error}`,
|
||||
iconName: icons.ui.warning,
|
||||
});
|
||||
errorHandler(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard that checks if a value is a non-null object
|
||||
*
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a non-null object
|
||||
*/
|
||||
private _isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
}
|
||||
271
src/lib/options/configManager/index.ts
Normal file
271
src/lib/options/configManager/index.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { readFile, writeFile, monitorFile, Gio } from 'astal/file';
|
||||
import { ensureDirectory } from '../../session';
|
||||
import icons from '../../icons/icons';
|
||||
import { errorHandler } from 'src/core/errors/handler';
|
||||
import { SystemUtilities } from 'src/core/system/SystemUtilities';
|
||||
|
||||
/**
|
||||
* Manages configuration file operations including reading, writing, and change monitoring
|
||||
*
|
||||
* Flow:
|
||||
* 1. Constructor creates config directory and starts file monitoring
|
||||
* 2. File monitor watches for external changes and triggers callbacks
|
||||
* 3. When writing config, monitor is canceled and recreated after delay
|
||||
* 4. External changes are debounced to prevent rapid callbacks
|
||||
* 5. Callbacks notify all registered listeners when config changes
|
||||
* 6. getNestedValue allows accessing deeply nested config values with dot notation
|
||||
*/
|
||||
export class ConfigManager {
|
||||
private static readonly _DEBOUNCE_DELAY_MS = 200;
|
||||
private static readonly _MONITOR_RESTART_DELAY_MS = 300;
|
||||
|
||||
private readonly _configPath: string;
|
||||
private readonly _changeCallbacks: Array<() => void> = [];
|
||||
private _fileMonitor: Gio.FileMonitor | null = null;
|
||||
private _lastChangeTime = 0;
|
||||
|
||||
/**
|
||||
* Creates a new configuration manager for a specific config file
|
||||
*
|
||||
* @param configPath - Full path to the configuration JSON file
|
||||
*/
|
||||
constructor(configPath: string) {
|
||||
this._configPath = configPath;
|
||||
this._createConfigDirectory();
|
||||
this._startConfigMonitoring();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a single option in the configuration file
|
||||
*
|
||||
* @param id - The option key to update
|
||||
* @param value - The new value to set
|
||||
*/
|
||||
public updateOption(id: string, value: unknown): void {
|
||||
const config = this.readConfig();
|
||||
config[id] = value;
|
||||
this.writeConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a value from a nested object using a path
|
||||
*
|
||||
* @param dataObject - The object to traverse
|
||||
* @param path - Dot-notation path (e.g., 'theme.colors.primary') or array of keys
|
||||
*/
|
||||
public getNestedValue(dataObject: Record<string, unknown>, path: string | string[]): unknown {
|
||||
const pathSegments = Array.isArray(path) ? path : path.split('.');
|
||||
return this._navigateToValue(dataObject, pathSegments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the current configuration from disk
|
||||
*/
|
||||
public readConfig(): Record<string, unknown> {
|
||||
const fileContent = readFile(this._configPath);
|
||||
|
||||
if (this._isEmptyOrMissing(fileContent)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return this._parseConfigSafely(fileContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes configuration to disk
|
||||
*
|
||||
* @param config - The configuration object to save
|
||||
*/
|
||||
public writeConfig(config: Record<string, unknown>): void {
|
||||
writeFile(this._configPath, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback to be called when the config file changes
|
||||
*
|
||||
* @param callback - Function to call when config changes
|
||||
*/
|
||||
public onConfigChanged(callback: () => void): void {
|
||||
this._changeCallbacks.push(callback);
|
||||
}
|
||||
|
||||
private _createConfigDirectory(): void {
|
||||
const directoryPath = this._getDirectoryPath();
|
||||
ensureDirectory(directoryPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the directory path from the full config file path
|
||||
*/
|
||||
private _getDirectoryPath(): string {
|
||||
return this._configPath.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up file monitoring to detect external changes to the config file
|
||||
*/
|
||||
private _startConfigMonitoring(): void {
|
||||
this._createFileMonitor();
|
||||
this._overrideWriteConfigForMonitoring();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file monitor, canceling any existing one first
|
||||
*
|
||||
* We must recreate the monitor after writes because the file system
|
||||
* monitor can become invalid when the file is replaced during write operations
|
||||
*/
|
||||
private _createFileMonitor(): void {
|
||||
this._cleanupExistingMonitor();
|
||||
|
||||
this._fileMonitor = monitorFile(this._configPath, () => {
|
||||
this._handleFileChange();
|
||||
});
|
||||
}
|
||||
|
||||
private _cleanupExistingMonitor(): void {
|
||||
if (!this._fileMonitor) return;
|
||||
|
||||
try {
|
||||
this._fileMonitor.cancel();
|
||||
} catch (error) {
|
||||
console.debug('Error canceling file monitor:', error);
|
||||
}
|
||||
|
||||
this._fileMonitor = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes file change events with debouncing to prevent rapid updates
|
||||
*/
|
||||
private _handleFileChange(): void {
|
||||
const now = Date.now();
|
||||
|
||||
if (this._shouldIgnoreChange(now)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lastChangeTime = now;
|
||||
this._notifyAllCallbacks();
|
||||
}
|
||||
|
||||
private _shouldIgnoreChange(currentTime: number): boolean {
|
||||
return currentTime - this._lastChangeTime < ConfigManager._DEBOUNCE_DELAY_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps writeConfig to automatically restart the file monitor after writes
|
||||
*
|
||||
* This ensures we don't miss external changes that occur immediately after
|
||||
* our own writes, which would otherwise be lost when the monitor is invalidated
|
||||
*/
|
||||
private _overrideWriteConfigForMonitoring(): void {
|
||||
const originalWriteConfig = this.writeConfig.bind(this);
|
||||
|
||||
this.writeConfig = (config: Record<string, unknown>): void => {
|
||||
originalWriteConfig(config);
|
||||
this._restartMonitoringAfterWrite();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules monitor recreation after a write operation
|
||||
*
|
||||
* The delay ensures the file system has finished processing the write
|
||||
* before we attach a new monitor, preventing race conditions
|
||||
*/
|
||||
private _restartMonitoringAfterWrite(): void {
|
||||
setTimeout(() => {
|
||||
this._createFileMonitor();
|
||||
}, ConfigManager._MONITOR_RESTART_DELAY_MS);
|
||||
}
|
||||
|
||||
private _isEmptyOrMissing(content: string): boolean {
|
||||
return !content || content.trim() === '';
|
||||
}
|
||||
|
||||
private _parseConfigSafely(content: string): Record<string, unknown> {
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
this._handleParsingError(error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively navigates an object to find a value at the specified path
|
||||
*
|
||||
* @param currentObject - The object to navigate
|
||||
* @param pathSegments - Array of keys representing the path
|
||||
*/
|
||||
private _navigateToValue(currentObject: Record<string, unknown>, pathSegments: string[]): unknown {
|
||||
if (pathSegments.length === 0) {
|
||||
return currentObject;
|
||||
}
|
||||
|
||||
if (!this._isValidObject(currentObject)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [currentKey, ...remainingPath] = pathSegments;
|
||||
const fullPath = [currentKey, ...remainingPath].join('.');
|
||||
|
||||
if (fullPath in currentObject) {
|
||||
return currentObject[fullPath];
|
||||
}
|
||||
|
||||
if (!(currentKey in currentObject)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nextValue = currentObject[currentKey];
|
||||
|
||||
if (!this._isValidObject(nextValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this._navigateToValue(nextValue, remainingPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all registered callbacks about config file changes
|
||||
*/
|
||||
private _notifyAllCallbacks(): void {
|
||||
this._changeCallbacks.forEach((callback) => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('Error in config change callback:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles configuration parsing errors with appropriate logging and notification
|
||||
*
|
||||
* @param error - The parsing error that occurred
|
||||
*/
|
||||
private _handleParsingError(error: unknown): void {
|
||||
const errorMessage = `Failed to load config file: ${error}`;
|
||||
|
||||
console.error(errorMessage);
|
||||
|
||||
SystemUtilities.notify({
|
||||
summary: 'Configuration Error',
|
||||
body: errorMessage,
|
||||
iconName: icons.ui.warning,
|
||||
});
|
||||
|
||||
errorHandler(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard that checks if a value is a valid object for navigation
|
||||
*
|
||||
* @param value - The value to check
|
||||
*/
|
||||
private _isValidObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ConfigManager } from './ConfigManager';
|
||||
import { Opt, OptProps } from './Opt';
|
||||
import { OptionRegistry } from './OptionRegistry';
|
||||
import { MkOptionsResult, OptionsObject } from './options.types';
|
||||
import { ConfigManager } from './configManager';
|
||||
import { Opt, OptProps } from './opt';
|
||||
import { OptionRegistry } from './optionRegistry';
|
||||
import { MkOptionsResult, OptionsObject } from './types';
|
||||
|
||||
const CONFIG_PATH = CONFIG_FILE;
|
||||
|
||||
@@ -19,7 +19,8 @@ export function opt<T>(initial: T, props?: OptProps): Opt<T> {
|
||||
*/
|
||||
export function mkOptions<T extends OptionsObject>(optionsObj: T): T & MkOptionsResult {
|
||||
const registry = new OptionRegistry(optionsObj, configManager);
|
||||
|
||||
return registry.createEnhancedOptions();
|
||||
}
|
||||
|
||||
export { Opt, OptProps, ConfigManager, OptionRegistry };
|
||||
export { Opt };
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
import Variable from 'astal/variable';
|
||||
import { ConfigManager } from './ConfigManager';
|
||||
|
||||
/**
|
||||
* Properties that can be passed when creating an option
|
||||
*/
|
||||
export interface OptProps {
|
||||
persistent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for set operations
|
||||
*/
|
||||
export interface WriteOptions {
|
||||
writeDisk?: boolean;
|
||||
}
|
||||
import { ConfigManager } from '../configManager';
|
||||
|
||||
/**
|
||||
* A managed application option with persistence capabilities
|
||||
@@ -96,3 +82,17 @@ export class Opt<T = unknown> extends Variable<T> {
|
||||
return currentValue !== initialValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties that can be passed when creating an option
|
||||
*/
|
||||
export interface OptProps {
|
||||
persistent?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for set operations
|
||||
*/
|
||||
interface WriteOptions {
|
||||
writeDisk?: boolean;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Opt } from './Opt';
|
||||
import { ConfigManager } from './ConfigManager';
|
||||
import { MkOptionsResult, OptionsObject } from './options.types';
|
||||
import { errorHandler } from '../utils';
|
||||
import { Opt } from '../opt';
|
||||
import { ConfigManager } from '../configManager';
|
||||
import { MkOptionsResult, OptionsObject } from '../types';
|
||||
import { errorHandler } from 'src/core/errors/handler';
|
||||
|
||||
/**
|
||||
* Creates and manages a registry of application options
|
||||
@@ -74,7 +74,11 @@ export class OptionRegistry<T extends OptionsObject> {
|
||||
}
|
||||
|
||||
const oldVal = opt.get();
|
||||
if (newVal !== oldVal) {
|
||||
|
||||
const newValueStringified = JSON.stringify(newVal, null, 2);
|
||||
const oldValueStringified = JSON.stringify(oldVal, null, 2);
|
||||
|
||||
if (newValueStringified !== oldValueStringified) {
|
||||
opt.set(newVal, { writeDisk: false });
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { dropdownMenuList } from '../constants/options';
|
||||
import { dropdownMenuList } from '../../components/settings/constants';
|
||||
import { FontStyle } from 'src/components/settings/shared/inputs/font/utils';
|
||||
import { Variable } from 'astal';
|
||||
import { defaultColorMap } from '../types/defaults/options.types';
|
||||
import { LabelSettingProps } from 'src/components/settings/shared/Label';
|
||||
import { Opt } from './Opt';
|
||||
import { Opt } from './opt';
|
||||
import { defaultColorMap } from 'src/services/matugen/defaults';
|
||||
|
||||
export interface MkOptionsResult {
|
||||
toArray: () => Opt[];
|
||||
@@ -12,17 +12,6 @@ export interface MkOptionsResult {
|
||||
handler: (optionsToWatch: string[], callback: () => void) => void;
|
||||
}
|
||||
|
||||
export type RecursiveOptionsObject = {
|
||||
[key: string]:
|
||||
| RecursiveOptionsObject
|
||||
| Opt<string>
|
||||
| Opt<number>
|
||||
| Opt<boolean>
|
||||
| Variable<string>
|
||||
| Variable<number>
|
||||
| Variable<boolean>;
|
||||
};
|
||||
|
||||
export type OptionsObject = Record<string, unknown>;
|
||||
|
||||
export type BarLocation = 'top' | 'bottom';
|
||||
@@ -62,7 +51,6 @@ export type BarLayouts = {
|
||||
[key: string]: BarLayout;
|
||||
};
|
||||
|
||||
export type Unit = 'imperial' | 'metric';
|
||||
export type PowerOptions = 'sleep' | 'reboot' | 'logout' | 'shutdown';
|
||||
export type NotificationAnchor =
|
||||
| 'top'
|
||||
@@ -88,7 +76,7 @@ export type ThemeExportData = {
|
||||
filePath: string;
|
||||
themeOnly: boolean;
|
||||
};
|
||||
export type InputType =
|
||||
type InputType =
|
||||
| 'number'
|
||||
| 'color'
|
||||
| 'float'
|
||||
@@ -266,12 +254,9 @@ export type MatugenVariations =
|
||||
| 'vivid_3';
|
||||
|
||||
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'
|
||||
32
src/lib/path/helpers.ts
Normal file
32
src/lib/path/helpers.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { GLib } from 'astal';
|
||||
|
||||
/**
|
||||
* Normalize a path to the absolute representation of the path
|
||||
* Note: This will only expand '~' if present. Path traversal is not supported
|
||||
* @param path - The path to normalize
|
||||
* @returns The normalized path
|
||||
*/
|
||||
export function normalizeToAbsolutePath(path: string): string {
|
||||
if (path.charAt(0) === '~') {
|
||||
return path.replace('~', GLib.get_home_dir());
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the home directory path
|
||||
* @returns The home directory path
|
||||
*/
|
||||
export function getHomeDir(): string {
|
||||
return GLib.get_home_dir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins path segments
|
||||
* @param segments - Path segments to join
|
||||
* @returns The joined path
|
||||
*/
|
||||
export function joinPath(...segments: string[]): string {
|
||||
return segments.join('/').replace(/\/+/g, '/');
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GenericFunction } from '../types/customModules/generic.types';
|
||||
import { BarModule } from '../options/options.types';
|
||||
import { BarModule } from '../options/types';
|
||||
import { Poller } from './Poller';
|
||||
import { Binding, execAsync, Variable } from 'astal';
|
||||
import { GenericFunction } from './types';
|
||||
|
||||
/**
|
||||
* A class that manages polling of a variable by executing a bash command at specified intervals.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GenericFunction } from '../types/customModules/generic.types';
|
||||
import { BarModule } from '../options/options.types';
|
||||
import { BarModule } from '../options/types';
|
||||
import { Poller } from './Poller';
|
||||
import { Binding, Variable } from 'astal';
|
||||
import { GenericFunction } from './types';
|
||||
|
||||
/**
|
||||
* A class that manages polling of a variable by executing a generic function at specified intervals.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getLayoutItems } from 'src/lib/utils';
|
||||
import { AstalIO, Binding, interval, Variable } from 'astal';
|
||||
import options from 'src/options';
|
||||
import { BarModule } from '../options/options.types';
|
||||
import options from 'src/configuration';
|
||||
import { BarModule } from '../options/types';
|
||||
import { getLayoutItems } from '../bar/helpers';
|
||||
|
||||
const { layouts } = options.bar;
|
||||
|
||||
|
||||
3
src/lib/poller/types.ts
Normal file
3
src/lib/poller/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type GenericFunction<Value, Parameters extends unknown[]> = (
|
||||
...args: Parameters
|
||||
) => Promise<Value> | Value;
|
||||
@@ -16,7 +16,7 @@ export function ensureDirectory(path: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureJsonFile(path: string): void {
|
||||
function ensureJsonFile(path: string): void {
|
||||
const file = Gio.File.new_for_path(path);
|
||||
const parent = file.get_parent();
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ensureJsonFile(path: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureFile(path: string): void {
|
||||
function ensureFile(path: string): void {
|
||||
const file = Gio.File.new_for_path(path);
|
||||
const parent = file.get_parent();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { ThrottleFn } from '../types/utils.types';
|
||||
import { GtkWidget } from '../types/widget.types';
|
||||
import { GtkWidget } from 'src/components/bar/types';
|
||||
import { ThrottleFn } from './types';
|
||||
|
||||
/**
|
||||
* Connects a primary click handler and returns a disconnect function.
|
||||
9
src/lib/shared/eventHandlers/types.ts
Normal file
9
src/lib/shared/eventHandlers/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Variable } from 'astal';
|
||||
import { EventArgs } from 'src/components/bar/utils/input/types';
|
||||
|
||||
export type ThrottleFn = (
|
||||
cmd: string,
|
||||
args: EventArgs,
|
||||
fn?: (output: string) => void,
|
||||
postInputUpdated?: Variable<boolean>,
|
||||
) => void;
|
||||
@@ -1,35 +0,0 @@
|
||||
import AstalMpris from 'gi://AstalMpris?version=0.1';
|
||||
|
||||
const mprisService = AstalMpris.get_default();
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
};
|
||||
65
src/lib/shared/notifications/index.ts
Normal file
65
src/lib/shared/notifications/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { Variable } from 'astal';
|
||||
import { iconExists } from 'src/lib/icons/helpers';
|
||||
import icons from 'src/lib/icons/icons';
|
||||
|
||||
const normalizeName = (name: string): string => name.toLowerCase().replace(/\s+/g, '_');
|
||||
|
||||
export const removingNotifications = Variable(false);
|
||||
|
||||
export const isNotificationIgnored = (
|
||||
notification: AstalNotifd.Notification | null,
|
||||
filter: string[],
|
||||
): boolean => {
|
||||
if (!notification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export const getNotificationIcon = (app_name: string, app_icon: string, app_entry: string): string => {
|
||||
const icon = icons.fallback.notification;
|
||||
|
||||
if (iconExists(app_name)) {
|
||||
return app_name;
|
||||
} else if (app_name && iconExists(app_name.toLowerCase())) {
|
||||
return app_name.toLowerCase();
|
||||
}
|
||||
|
||||
if (app_icon && iconExists(app_icon)) {
|
||||
return app_icon;
|
||||
}
|
||||
|
||||
if (app_entry && iconExists(app_entry)) {
|
||||
return app_entry;
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const clearNotifications = async (
|
||||
notifications: AstalNotifd.Notification[],
|
||||
delay: number,
|
||||
): Promise<void> => {
|
||||
removingNotifications.set(true);
|
||||
for (const notification of notifications) {
|
||||
notification.dismiss();
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
removingNotifications.set(false);
|
||||
};
|
||||
92
src/lib/string/formatters.ts
Normal file
92
src/lib/string/formatters.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Capitalizes the first letter of a string
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to camelCase
|
||||
* @param str - The string to convert
|
||||
* @returns The camelCase version of the string
|
||||
*/
|
||||
export function toCamelCase(str: string): string {
|
||||
return str
|
||||
.replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
|
||||
.replace(/^(.)/, (char) => char.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to kebab-case
|
||||
* @param str - The string to convert
|
||||
* @returns The kebab-case version of the string
|
||||
*/
|
||||
export function toKebabCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||
.replace(/[\s_]+/g, '-')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to Title Case
|
||||
* @param str - The string to convert
|
||||
* @returns The Title Case version of the string
|
||||
*/
|
||||
export function toTitleCase(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/(?:^|\s|-|_)\w/g, (match) => match.toUpperCase())
|
||||
.replace(/[-_]/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to snake_case
|
||||
* @param str - The string to convert
|
||||
* @returns The snake_case version of the string
|
||||
*/
|
||||
export function toSnakeCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
||||
.replace(/[-\s]+/g, '_')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to PascalCase
|
||||
* @param str - The string to convert
|
||||
* @returns The PascalCase version of the string
|
||||
*/
|
||||
export function toPascalCase(str: string): string {
|
||||
return str
|
||||
.replace(/[-_\s]+(.)?/g, (_, char) => (char ? char.toUpperCase() : ''))
|
||||
.replace(/^(.)/, (char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to CONSTANT_CASE
|
||||
* @param str - The string to convert
|
||||
* @returns The CONSTANT_CASE version of the string
|
||||
*/
|
||||
export function toConstantCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
||||
.replace(/[-\s]+/g, '_')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to sentence case
|
||||
* @param str - The string to convert
|
||||
* @returns The sentence case version of the string
|
||||
*/
|
||||
export function toSentenceCase(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[-_]/g, ' ')
|
||||
.replace(/^\w/, (char) => char.toUpperCase())
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
40
src/lib/theme/useTheme.ts
Normal file
40
src/lib/theme/useTheme.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import Gio from 'gi://Gio';
|
||||
import {
|
||||
filterConfigForThemeOnly,
|
||||
loadJsonFile,
|
||||
saveConfigToFile,
|
||||
} from '../../components/settings/shared/FileChooser';
|
||||
import options from 'src/configuration';
|
||||
import { errorHandler } from 'src/core/errors/handler';
|
||||
import { SystemUtilities } from 'src/core/system/SystemUtilities';
|
||||
|
||||
const { restartCommand } = options.hyprpanel;
|
||||
export const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
||||
|
||||
export function useTheme(filePath: string): void {
|
||||
try {
|
||||
const importedConfig = loadJsonFile(filePath);
|
||||
|
||||
if (!importedConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionsConfigFile = Gio.File.new_for_path(CONFIG_FILE);
|
||||
|
||||
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
|
||||
|
||||
if (!optionsSuccess) {
|
||||
throw new Error('Failed to load theme file.');
|
||||
}
|
||||
|
||||
let optionsConfig = JSON.parse(new TextDecoder('utf-8').decode(optionsContent));
|
||||
|
||||
const filteredConfig = filterConfigForThemeOnly(importedConfig);
|
||||
optionsConfig = { ...optionsConfig, ...filteredConfig };
|
||||
|
||||
saveConfigToFile(optionsConfig, CONFIG_FILE);
|
||||
SystemUtilities.bash(restartCommand.get());
|
||||
} catch (error) {
|
||||
errorHandler(error);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export type MediaTags = {
|
||||
title: string;
|
||||
artists: string;
|
||||
artist: string;
|
||||
album: string;
|
||||
name: string;
|
||||
identity: string;
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Widget } from 'astal/gtk3';
|
||||
import { Binding, Variable } from 'astal';
|
||||
import { Connectable } from 'astal/binding';
|
||||
import { BoxWidget } from './widget.types';
|
||||
import { Label } from 'astal/gtk3/widget';
|
||||
|
||||
export type BarBoxChild = {
|
||||
component: JSX.Element;
|
||||
isVisible?: boolean;
|
||||
isVis?: Binding<boolean>;
|
||||
isBox?: boolean;
|
||||
boxClass: string;
|
||||
tooltip_text?: string | Binding<string>;
|
||||
} & ({ isBox: true; props: Widget.EventBoxProps } | { isBox?: false; props: Widget.ButtonProps });
|
||||
|
||||
export type BoxHook = (self: BoxWidget) => void;
|
||||
export type LabelHook = (self: Label) => void;
|
||||
|
||||
export type BarModuleProps = {
|
||||
icon?: string | Binding<string>;
|
||||
textIcon?: string | Binding<string>;
|
||||
useTextIcon?: Binding<boolean>;
|
||||
label?: string | Binding<string>;
|
||||
truncationSize?: Binding<number>;
|
||||
labelHook?: LabelHook;
|
||||
boundLabel?: string;
|
||||
tooltipText?: string | Binding<string>;
|
||||
boxClass: string;
|
||||
isVis?: Binding<boolean>;
|
||||
props?: Widget.ButtonProps;
|
||||
showLabel?: boolean;
|
||||
showLabelBinding?: Binding<boolean>;
|
||||
showIconBinding?: Binding<boolean>;
|
||||
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';
|
||||
@@ -1,5 +0,0 @@
|
||||
export type BatteryIconKeys = 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100;
|
||||
|
||||
export type BatteryIcons = {
|
||||
[key in BatteryIconKeys]: string;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export type BarToggleStates = Record<string, boolean | undefined>;
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
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[];
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export type NetworkResourceData = {
|
||||
in: string;
|
||||
out: string;
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Variable } from 'astal';
|
||||
import { EventArgs } from '../widget.types';
|
||||
import { Opt } from 'src/lib/options';
|
||||
|
||||
export type InputHandlerEventArgs = {
|
||||
cmd?: Opt<string> | Variable<string>;
|
||||
fn?: (output: string) => void;
|
||||
};
|
||||
export type InputHandlerEvents = {
|
||||
onPrimaryClick?: InputHandlerEventArgs;
|
||||
onSecondaryClick?: InputHandlerEventArgs;
|
||||
onMiddleClick?: InputHandlerEventArgs;
|
||||
onScrollUp?: InputHandlerEventArgs;
|
||||
onScrollDown?: InputHandlerEventArgs;
|
||||
};
|
||||
|
||||
export type RunAsyncCommand = (
|
||||
cmd: string,
|
||||
args: EventArgs,
|
||||
fn?: (output: string) => void,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
) => void;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Variable } from 'astal';
|
||||
|
||||
export type ShortcutFixed = {
|
||||
tooltip: string;
|
||||
command: string;
|
||||
icon: string;
|
||||
configurable: false;
|
||||
};
|
||||
|
||||
export type ShortcutVariable = {
|
||||
tooltip: Variable<string>;
|
||||
command: Variable<string>;
|
||||
icon: Variable<string>;
|
||||
configurable?: true;
|
||||
};
|
||||
|
||||
export type Shortcut = ShortcutFixed | ShortcutVariable;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { NetstatLabelType, ResourceLabelType } from '../bar.types';
|
||||
import { BarLocation } from '../../options/options.types';
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { RateUnit } from '../bar.types';
|
||||
import { NetworkResourceData } from '../customModules/network.types';
|
||||
|
||||
export const getDefaultNetstatData = (dataType: RateUnit): NetworkResourceData => {
|
||||
if (dataType === 'auto') {
|
||||
return { in: `0 Kib/s`, out: `0 Kib/s` };
|
||||
}
|
||||
|
||||
return { in: `0 ${dataType}/s`, out: `0 ${dataType}/s` };
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
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
@@ -1,13 +0,0 @@
|
||||
import { Astal, Gtk } from 'astal/gtk3';
|
||||
import { Binding } from 'astal';
|
||||
import { WindowProps } from 'astal/gtk3/widget';
|
||||
|
||||
export interface DropdownMenuProps {
|
||||
name: string;
|
||||
child?: JSX.Element | JSX.Element[] | Binding<JSX.Element | undefined>;
|
||||
layout?: string;
|
||||
transition?: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>;
|
||||
exclusivity?: Astal.Exclusivity;
|
||||
fixed?: boolean;
|
||||
onDestroy?: () => void;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export type Config = {
|
||||
[key: string]: string | number | boolean | object;
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Process } from 'astal';
|
||||
|
||||
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[];
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import AstalMpris from 'gi://AstalMpris?version=0.1';
|
||||
|
||||
export type PlaybackIconMap = {
|
||||
[key in AstalMpris.PlaybackStatus]: string;
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
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 WifiIcon = '' | '' | '' | '' | '' | '' | '' | '' | '' | '' | '';
|
||||
@@ -1,15 +0,0 @@
|
||||
import icons from 'src/lib/icons/icons2';
|
||||
|
||||
export interface NotificationArgs {
|
||||
appName?: string;
|
||||
body?: string;
|
||||
iconName?: string;
|
||||
id?: number;
|
||||
summary?: string;
|
||||
urgency?: string;
|
||||
category?: string;
|
||||
timeout?: number;
|
||||
transient?: boolean;
|
||||
}
|
||||
|
||||
export type NotificationIcon = keyof typeof icons.notifications;
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Astal, Gtk } from 'astal/gtk3';
|
||||
import { Binding } from 'astal';
|
||||
import { WindowProps } from 'astal/gtk3/widget';
|
||||
|
||||
export interface PopupWindowProps extends WindowProps {
|
||||
name: string;
|
||||
child?: JSX.Element;
|
||||
layout?: Layouts;
|
||||
transition?: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>;
|
||||
exclusivity?: Astal.Exclusivity;
|
||||
}
|
||||
|
||||
export type LayoutFunction = (
|
||||
name: string,
|
||||
child: JSX.Element,
|
||||
transition: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>,
|
||||
) => {
|
||||
center: () => JSX.Element;
|
||||
top: () => JSX.Element;
|
||||
'top-right': () => JSX.Element;
|
||||
'top-center': () => JSX.Element;
|
||||
'top-left': () => JSX.Element;
|
||||
'bottom-left': () => JSX.Element;
|
||||
'bottom-center': () => JSX.Element;
|
||||
'bottom-right': () => JSX.Element;
|
||||
};
|
||||
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: JSX.Element;
|
||||
transition: Gtk.RevealerTransitionType | Binding<Gtk.RevealerTransitionType>;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export type Action = 'sleep' | 'reboot' | 'logout' | 'shutdown';
|
||||
@@ -1 +0,0 @@
|
||||
export type ProfileType = 'balanced' | 'power-saver' | 'performance';
|
||||
@@ -1,6 +0,0 @@
|
||||
export type SystrayIconMap = {
|
||||
[key: string]: {
|
||||
icon: string;
|
||||
color: string;
|
||||
};
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Variable } from 'astal';
|
||||
import { EventArgs } from './widget.types';
|
||||
|
||||
export type ThrottleFn = (
|
||||
cmd: string,
|
||||
args: EventArgs,
|
||||
fn?: (output: string) => void,
|
||||
postInputUpdated?: Variable<boolean>,
|
||||
) => void;
|
||||
|
||||
export type ThrottleFnCallback = ((output: string) => void) | undefined;
|
||||
|
||||
export type Primitive = string | number | boolean | symbol | null | undefined | bigint;
|
||||
@@ -1,118 +0,0 @@
|
||||
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];
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Binding } from 'astal';
|
||||
import { Astal, Gdk, Gtk } from 'astal/gtk3';
|
||||
|
||||
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 BoxWidget = Gtk.Box;
|
||||
export type GdkEvent = Gdk.Event;
|
||||
|
||||
export type EventHandler<Self> = (self: Self, event: Gdk.Event) => boolean | unknown;
|
||||
export type EventArgs = {
|
||||
clicked: GtkWidget;
|
||||
event: Gdk.Event;
|
||||
};
|
||||
|
||||
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) => Binding<boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
export type GtkWidget = GtkWidgetExtended;
|
||||
@@ -1,36 +0,0 @@
|
||||
export type WorkspaceRule = {
|
||||
workspaceString: string;
|
||||
monitor: string;
|
||||
};
|
||||
|
||||
export type WorkspaceMonitorMap = {
|
||||
[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;
|
||||
196
src/lib/units/length/index.ts
Normal file
196
src/lib/units/length/index.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { LengthUnit } from './types';
|
||||
|
||||
export class LengthConverter {
|
||||
private readonly _value: number;
|
||||
private readonly _unit: LengthUnit;
|
||||
|
||||
private static readonly _TO_METERS: Record<LengthUnit, number> = {
|
||||
mm: 0.001,
|
||||
cm: 0.01,
|
||||
m: 1,
|
||||
km: 1000,
|
||||
in: 0.0254,
|
||||
ft: 0.3048,
|
||||
mi: 1609.344,
|
||||
};
|
||||
|
||||
private constructor(value: number, unit: LengthUnit) {
|
||||
this._value = value;
|
||||
this._unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from millimeters
|
||||
* @param value - Value in millimeters
|
||||
*/
|
||||
public static fromMm(value: number): LengthConverter {
|
||||
return new LengthConverter(value, 'mm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from centimeters
|
||||
* @param value - Value in centimeters
|
||||
*/
|
||||
public static fromCm(value: number): LengthConverter {
|
||||
return new LengthConverter(value, 'cm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from meters
|
||||
* @param value - Value in meters
|
||||
*/
|
||||
public static fromMeters(value: number): LengthConverter {
|
||||
return new LengthConverter(value, 'm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from kilometers
|
||||
* @param value - Value in kilometers
|
||||
*/
|
||||
public static fromKm(value: number): LengthConverter {
|
||||
return new LengthConverter(value, 'km');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from inches
|
||||
* @param value - Value in inches
|
||||
*/
|
||||
public static fromInches(value: number): LengthConverter {
|
||||
return new LengthConverter(value, 'in');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from feet
|
||||
* @param value - Value in feet
|
||||
*/
|
||||
public static fromFeet(value: number): LengthConverter {
|
||||
return new LengthConverter(value, 'ft');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from miles
|
||||
* @param value - Value in miles
|
||||
*/
|
||||
public static fromMiles(value: number): LengthConverter {
|
||||
return new LengthConverter(value, 'mi');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to meters (base unit)
|
||||
*/
|
||||
private _toBaseUnit(): number {
|
||||
return this._value * LengthConverter._TO_METERS[this._unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from meters to target unit
|
||||
*/
|
||||
private _fromBaseUnit(targetUnit: LengthUnit): number {
|
||||
return this._toBaseUnit() / LengthConverter._TO_METERS[targetUnit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to millimeters
|
||||
*/
|
||||
public toMm(): number {
|
||||
return this._fromBaseUnit('mm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to centimeters
|
||||
*/
|
||||
public toCm(): number {
|
||||
return this._fromBaseUnit('cm');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to meters
|
||||
*/
|
||||
public toMeters(): number {
|
||||
return this._toBaseUnit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to kilometers
|
||||
*/
|
||||
public toKm(): number {
|
||||
return this._fromBaseUnit('km');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to inches
|
||||
*/
|
||||
public toInches(): number {
|
||||
return this._fromBaseUnit('in');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to feet
|
||||
*/
|
||||
public toFeet(): number {
|
||||
return this._fromBaseUnit('ft');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to miles
|
||||
*/
|
||||
public toMiles(): number {
|
||||
return this._fromBaseUnit('mi');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to millimeters
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatMm(precision = 0): string {
|
||||
return `${this.toMm().toFixed(precision)} mm`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to centimeters
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatCm(precision = 1): string {
|
||||
return `${this.toCm().toFixed(precision)} cm`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to meters
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatMeters(precision = 2): string {
|
||||
return `${this.toMeters().toFixed(precision)} m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to kilometers
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatKm(precision = 1): string {
|
||||
return `${this.toKm().toFixed(precision)} km`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to inches
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatInches(precision = 1): string {
|
||||
return `${this.toInches().toFixed(precision)} in`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to feet
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatFeet(precision = 0): string {
|
||||
return `${this.toFeet().toFixed(precision)} ft`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to miles
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatMiles(precision = 1): string {
|
||||
return `${this.toMiles().toFixed(precision)} mi`;
|
||||
}
|
||||
}
|
||||
1
src/lib/units/length/types.ts
Normal file
1
src/lib/units/length/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type LengthUnit = 'mm' | 'cm' | 'm' | 'km' | 'in' | 'ft' | 'mi';
|
||||
148
src/lib/units/pressure/index.ts
Normal file
148
src/lib/units/pressure/index.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { PressureUnit } from './types';
|
||||
|
||||
export class PressureConverter {
|
||||
private readonly _value: number;
|
||||
private readonly _unit: PressureUnit;
|
||||
|
||||
private static readonly _TO_PA: Record<PressureUnit, number> = {
|
||||
pa: 1,
|
||||
hpa: 100,
|
||||
mb: 100,
|
||||
inHg: 3386.39,
|
||||
psi: 6894.76,
|
||||
};
|
||||
|
||||
private constructor(value: number, unit: PressureUnit) {
|
||||
this._value = value;
|
||||
this._unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from pascals
|
||||
* @param value - Value in pascals
|
||||
*/
|
||||
public static fromPa(value: number): PressureConverter {
|
||||
return new PressureConverter(value, 'pa');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from hectopascals
|
||||
* @param value - Value in hectopascals
|
||||
*/
|
||||
public static fromHPa(value: number): PressureConverter {
|
||||
return new PressureConverter(value, 'hpa');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from millibars
|
||||
* @param value - Value in millibars
|
||||
*/
|
||||
public static fromMb(value: number): PressureConverter {
|
||||
return new PressureConverter(value, 'mb');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from inches of mercury
|
||||
* @param value - Value in inches of mercury
|
||||
*/
|
||||
public static fromInHg(value: number): PressureConverter {
|
||||
return new PressureConverter(value, 'inHg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from pounds per square inch
|
||||
* @param value - Value in PSI
|
||||
*/
|
||||
public static fromPsi(value: number): PressureConverter {
|
||||
return new PressureConverter(value, 'psi');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to pascals (base unit)
|
||||
*/
|
||||
private _toBaseUnit(): number {
|
||||
return this._value * PressureConverter._TO_PA[this._unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from pascals to target unit
|
||||
*/
|
||||
private _fromBaseUnit(targetUnit: PressureUnit): number {
|
||||
return this._toBaseUnit() / PressureConverter._TO_PA[targetUnit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to pascals
|
||||
*/
|
||||
public toPa(): number {
|
||||
return this._toBaseUnit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to hectopascals
|
||||
*/
|
||||
public toHPa(): number {
|
||||
return this._fromBaseUnit('hpa');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to millibars
|
||||
*/
|
||||
public toMb(): number {
|
||||
return this._fromBaseUnit('mb');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to inches of mercury
|
||||
*/
|
||||
public toInHg(): number {
|
||||
return this._fromBaseUnit('inHg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to pounds per square inch
|
||||
*/
|
||||
public toPsi(): number {
|
||||
return this._fromBaseUnit('psi');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to pascals
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatPa(precision = 0): string {
|
||||
return `${this.toPa().toFixed(precision)} Pa`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to hectopascals
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatHPa(precision = 0): string {
|
||||
return `${this.toHPa().toFixed(precision)} hPa`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to millibars
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatMb(precision = 0): string {
|
||||
return `${this.toMb().toFixed(precision)} mb`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to inches of mercury
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatInHg(precision = 2): string {
|
||||
return `${this.toInHg().toFixed(precision)} inHg`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to pounds per square inch
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatPsi(precision = 1): string {
|
||||
return `${this.toPsi().toFixed(precision)} PSI`;
|
||||
}
|
||||
}
|
||||
1
src/lib/units/pressure/types.ts
Normal file
1
src/lib/units/pressure/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type PressureUnit = 'pa' | 'hpa' | 'mb' | 'inHg' | 'psi';
|
||||
225
src/lib/units/size/index.ts
Normal file
225
src/lib/units/size/index.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { SizeUnit } from './types';
|
||||
|
||||
export class SizeConverter {
|
||||
private readonly _value: number;
|
||||
private readonly _unit: SizeUnit;
|
||||
|
||||
private constructor(value: number, unit: SizeUnit) {
|
||||
this._value = value;
|
||||
this._unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from bytes
|
||||
* @param value - Size in bytes
|
||||
*/
|
||||
public static fromBytes(value: number): SizeConverter {
|
||||
return new SizeConverter(value, 'bytes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from kibibytes
|
||||
* @param value - Size in KiB
|
||||
*/
|
||||
public static fromKiB(value: number): SizeConverter {
|
||||
return new SizeConverter(value, 'kibibytes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from mebibytes
|
||||
* @param value - Size in MiB
|
||||
*/
|
||||
public static fromMiB(value: number): SizeConverter {
|
||||
return new SizeConverter(value, 'mebibytes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from gibibytes
|
||||
* @param value - Size in GiB
|
||||
*/
|
||||
public static fromGiB(value: number): SizeConverter {
|
||||
return new SizeConverter(value, 'gibibytes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from tebibytes
|
||||
* @param value - Size in TiB
|
||||
*/
|
||||
public static fromTiB(value: number): SizeConverter {
|
||||
return new SizeConverter(value, 'tebibytes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the size to bytes (base unit)
|
||||
*/
|
||||
private _toBaseUnit(): number {
|
||||
switch (this._unit) {
|
||||
case 'bytes':
|
||||
return this._value;
|
||||
case 'kibibytes':
|
||||
return this._value * 1024;
|
||||
case 'mebibytes':
|
||||
return this._value * 1024 ** 2;
|
||||
case 'gibibytes':
|
||||
return this._value * 1024 ** 3;
|
||||
case 'tebibytes':
|
||||
return this._value * 1024 ** 4;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to bytes
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toBytes(precision?: number): number {
|
||||
const value = this._toBaseUnit();
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to kibibytes
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toKiB(precision?: number): number {
|
||||
const bytes = this._toBaseUnit();
|
||||
const value = bytes / 1024;
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to mebibytes
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toMiB(precision?: number): number {
|
||||
const bytes = this._toBaseUnit();
|
||||
const value = bytes / 1024 ** 2;
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to gibibytes
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toGiB(precision?: number): number {
|
||||
const bytes = this._toBaseUnit();
|
||||
const value = bytes / 1024 ** 3;
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to tebibytes
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toTiB(precision?: number): number {
|
||||
const bytes = this._toBaseUnit();
|
||||
const value = bytes / 1024 ** 4;
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically converts to the most appropriate unit
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toAuto(precision?: number): { value: number; unit: SizeUnit } {
|
||||
const bytes = this._toBaseUnit();
|
||||
|
||||
if (bytes >= 1024 ** 4) {
|
||||
return { value: this.toTiB(precision), unit: 'tebibytes' };
|
||||
}
|
||||
if (bytes >= 1024 ** 3) {
|
||||
return { value: this.toGiB(precision), unit: 'gibibytes' };
|
||||
}
|
||||
if (bytes >= 1024 ** 2) {
|
||||
return { value: this.toMiB(precision), unit: 'mebibytes' };
|
||||
}
|
||||
if (bytes >= 1024) {
|
||||
return { value: this.toKiB(precision), unit: 'kibibytes' };
|
||||
}
|
||||
|
||||
return { value: this.toBytes(precision), unit: 'bytes' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the size with a specific unit and precision
|
||||
* @param unit - Target unit
|
||||
* @param precision - Number of decimal places (default: 2)
|
||||
*/
|
||||
public format(unit: SizeUnit, precision = 2): string {
|
||||
let value: number;
|
||||
let symbol: string;
|
||||
|
||||
switch (unit) {
|
||||
case 'bytes':
|
||||
value = this.toBytes();
|
||||
symbol = 'B';
|
||||
break;
|
||||
case 'kibibytes':
|
||||
value = this.toKiB();
|
||||
symbol = 'KiB';
|
||||
break;
|
||||
case 'mebibytes':
|
||||
value = this.toMiB();
|
||||
symbol = 'MiB';
|
||||
break;
|
||||
case 'gibibytes':
|
||||
value = this.toGiB();
|
||||
symbol = 'GiB';
|
||||
break;
|
||||
case 'tebibytes':
|
||||
value = this.toTiB();
|
||||
symbol = 'TiB';
|
||||
break;
|
||||
}
|
||||
|
||||
return `${value.toFixed(precision)} ${symbol}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to bytes
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatBytes(precision = 0): string {
|
||||
return this.format('bytes', precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to kibibytes
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatKiB(precision = 2): string {
|
||||
return this.format('kibibytes', precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to mebibytes
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatMiB(precision = 2): string {
|
||||
return this.format('mebibytes', precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to gibibytes
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatGiB(precision = 2): string {
|
||||
return this.format('gibibytes', precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to tebibytes
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatTiB(precision = 2): string {
|
||||
return this.format('tebibytes', precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically formats to the most appropriate unit
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatAuto(precision = 2): string {
|
||||
const { unit } = this.toAuto();
|
||||
return this.format(unit, precision);
|
||||
}
|
||||
}
|
||||
1
src/lib/units/size/types.ts
Normal file
1
src/lib/units/size/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type SizeUnit = 'bytes' | 'kibibytes' | 'mebibytes' | 'gibibytes' | 'tebibytes';
|
||||
131
src/lib/units/speed/index.ts
Normal file
131
src/lib/units/speed/index.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { SpeedUnit } from './types';
|
||||
|
||||
export class SpeedConverter {
|
||||
private readonly _value: number;
|
||||
private readonly _unit: SpeedUnit;
|
||||
|
||||
private static readonly _TO_MPS: Record<SpeedUnit, number> = {
|
||||
mps: 1,
|
||||
kph: 0.277778,
|
||||
mph: 0.44704,
|
||||
knots: 0.514444,
|
||||
};
|
||||
|
||||
private static readonly _LABELS: Record<SpeedUnit, string> = {
|
||||
mps: 'm/s',
|
||||
kph: 'km/h',
|
||||
mph: 'mph',
|
||||
knots: 'kn',
|
||||
};
|
||||
|
||||
private constructor(value: number, unit: SpeedUnit) {
|
||||
this._value = value;
|
||||
this._unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from meters per second
|
||||
* @param value - Value in m/s
|
||||
*/
|
||||
public static fromMps(value: number): SpeedConverter {
|
||||
return new SpeedConverter(value, 'mps');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from kilometers per hour
|
||||
* @param value - Value in km/h
|
||||
*/
|
||||
public static fromKph(value: number): SpeedConverter {
|
||||
return new SpeedConverter(value, 'kph');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from miles per hour
|
||||
* @param value - Value in mph
|
||||
*/
|
||||
public static fromMph(value: number): SpeedConverter {
|
||||
return new SpeedConverter(value, 'mph');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from knots
|
||||
* @param value - Value in knots
|
||||
*/
|
||||
public static fromKnots(value: number): SpeedConverter {
|
||||
return new SpeedConverter(value, 'knots');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to m/s (base unit)
|
||||
*/
|
||||
private _toBaseUnit(): number {
|
||||
return this._value * SpeedConverter._TO_MPS[this._unit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from m/s to target unit
|
||||
*/
|
||||
private _fromBaseUnit(targetUnit: SpeedUnit): number {
|
||||
return this._toBaseUnit() / SpeedConverter._TO_MPS[targetUnit];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to meters per second
|
||||
*/
|
||||
public toMps(): number {
|
||||
return this._toBaseUnit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to kilometers per hour
|
||||
*/
|
||||
public toKph(): number {
|
||||
return this._fromBaseUnit('kph');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to miles per hour
|
||||
*/
|
||||
public toMph(): number {
|
||||
return this._fromBaseUnit('mph');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to knots
|
||||
*/
|
||||
public toKnots(): number {
|
||||
return this._fromBaseUnit('knots');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to meters per second
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatMps(precision = 1): string {
|
||||
return `${this.toMps().toFixed(precision)} ${SpeedConverter._LABELS.mps}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to kilometers per hour
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatKph(precision = 0): string {
|
||||
return `${this.toKph().toFixed(precision)} ${SpeedConverter._LABELS.kph}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to miles per hour
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatMph(precision = 0): string {
|
||||
return `${this.toMph().toFixed(precision)} ${SpeedConverter._LABELS.mph}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to knots
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatKnots(precision = 0): string {
|
||||
return `${this.toKnots().toFixed(precision)} ${SpeedConverter._LABELS.knots}`;
|
||||
}
|
||||
}
|
||||
1
src/lib/units/speed/types.ts
Normal file
1
src/lib/units/speed/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type SpeedUnit = 'mps' | 'kph' | 'mph' | 'knots';
|
||||
129
src/lib/units/temperature/index.ts
Normal file
129
src/lib/units/temperature/index.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { TemperatureUnit } from './types';
|
||||
|
||||
export class TemperatureConverter {
|
||||
private readonly _value: number;
|
||||
private readonly _unit: TemperatureUnit;
|
||||
|
||||
private constructor(value: number, unit: TemperatureUnit) {
|
||||
this._value = value;
|
||||
this._unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from Celsius
|
||||
* @param value - Temperature in Celsius
|
||||
*/
|
||||
public static fromCelsius(value: number): TemperatureConverter {
|
||||
return new TemperatureConverter(value, 'celsius');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from Fahrenheit
|
||||
* @param value - Temperature in Fahrenheit
|
||||
*/
|
||||
public static fromFahrenheit(value: number): TemperatureConverter {
|
||||
return new TemperatureConverter(value, 'fahrenheit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a converter from Kelvin
|
||||
* @param value - Temperature in Kelvin
|
||||
*/
|
||||
public static fromKelvin(value: number): TemperatureConverter {
|
||||
return new TemperatureConverter(value, 'kelvin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the temperature to Celsius (base unit)
|
||||
*/
|
||||
private _toBaseUnit(): number {
|
||||
switch (this._unit) {
|
||||
case 'celsius':
|
||||
return this._value;
|
||||
case 'fahrenheit':
|
||||
return ((this._value - 32) * 5) / 9;
|
||||
case 'kelvin':
|
||||
return this._value - 273.15;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to Celsius
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toCelsius(precision?: number): number {
|
||||
const value = this._toBaseUnit();
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to Fahrenheit
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toFahrenheit(precision?: number): number {
|
||||
const celsius = this._toBaseUnit();
|
||||
const value = (celsius * 9) / 5 + 32;
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to Kelvin
|
||||
* @param precision - Number of decimal places (optional)
|
||||
*/
|
||||
public toKelvin(precision?: number): number {
|
||||
const celsius = this._toBaseUnit();
|
||||
const value = celsius + 273.15;
|
||||
return precision !== undefined ? Number(value.toFixed(precision)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the temperature with a specific unit and precision
|
||||
* @param unit - Target unit
|
||||
* @param precision - Number of decimal places (default: 0)
|
||||
*/
|
||||
public format(unit: TemperatureUnit, precision = 0): string {
|
||||
let value: number;
|
||||
let symbol: string;
|
||||
|
||||
switch (unit) {
|
||||
case 'celsius':
|
||||
value = this.toCelsius();
|
||||
symbol = '° C';
|
||||
break;
|
||||
case 'fahrenheit':
|
||||
value = this.toFahrenheit();
|
||||
symbol = '° F';
|
||||
break;
|
||||
case 'kelvin':
|
||||
value = this.toKelvin();
|
||||
symbol = ' K';
|
||||
break;
|
||||
}
|
||||
|
||||
return `${value.toFixed(precision)}${symbol}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to Celsius
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatCelsius(precision = 0): string {
|
||||
return this.format('celsius', precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to Fahrenheit
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatFahrenheit(precision = 0): string {
|
||||
return this.format('fahrenheit', precision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats to Kelvin
|
||||
* @param precision - Number of decimal places
|
||||
*/
|
||||
public formatKelvin(precision = 0): string {
|
||||
return this.format('kelvin', precision);
|
||||
}
|
||||
}
|
||||
2
src/lib/units/temperature/types.ts
Normal file
2
src/lib/units/temperature/types.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type UnitType = 'imperial' | 'metric';
|
||||
export type TemperatureUnit = 'celsius' | 'fahrenheit' | 'kelvin';
|
||||
6
src/lib/units/time/index.ts
Normal file
6
src/lib/units/time/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { GLib, Variable } from 'astal';
|
||||
|
||||
export const systemTime = Variable(GLib.DateTime.new_now_local()).poll(
|
||||
1000,
|
||||
(): GLib.DateTime => GLib.DateTime.new_now_local(),
|
||||
);
|
||||
425
src/lib/utils.ts
425
src/lib/utils.ts
@@ -1,425 +0,0 @@
|
||||
import { BarModule, NotificationAnchor, PositionAnchor } from './options/options.types';
|
||||
import { OSDAnchor } from './options/options.types';
|
||||
import icons from './icons/icons';
|
||||
import GLib from 'gi://GLib?version=2.0';
|
||||
import GdkPixbuf from 'gi://GdkPixbuf';
|
||||
import { NotificationArgs } from './types/notification.types';
|
||||
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 { exec, execAsync } from 'astal/process';
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { Primitive } from './types/utils.types';
|
||||
|
||||
const notifdService = AstalNotifd.get_default();
|
||||
|
||||
/**
|
||||
* Checks if a value is a primitive type.
|
||||
*
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a primitive (null, undefined, string, number, boolean, symbol, or bigint)
|
||||
*/
|
||||
export function isPrimitive(value: unknown): value is Primitive {
|
||||
return value === null || (typeof value !== 'object' && typeof value !== 'function');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 === undefined) 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)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 stringsIsString = typeof strings === 'string';
|
||||
const cmd = stringsIsString ? 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 async function forMonitors(widget: (monitor: number) => Promise<JSX.Element>): Promise<JSX.Element[]> {
|
||||
const n = Gdk.Display.get_default()?.get_n_monitors() ?? 1;
|
||||
|
||||
return Promise.all(range(n, 0).map(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,
|
||||
});
|
||||
}
|
||||
|
||||
return missing.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Note: Unlike GdkPixbuf, this function will normalize the given path.
|
||||
*
|
||||
* @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 {
|
||||
GdkPixbuf.Pixbuf.new_from_file(normalizePath(imgFilePath));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.info(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a path to the absolute representation of the path.
|
||||
*
|
||||
* Note: This will only expand '~' if present. Path traversal is not supported.
|
||||
*
|
||||
* @param path The path to normalize.
|
||||
*
|
||||
* @returns The normalized path.
|
||||
*/
|
||||
export function normalizePath(path: string): string {
|
||||
if (path.charAt(0) == '~') {
|
||||
// Replace will only replace the first match, in this case, the first character
|
||||
return path.replace('~', GLib.get_home_dir());
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// This line does nothing useful at runtime, but when bundling, it
|
||||
// ensures that notifdService has been instantiated and, as such,
|
||||
// that the notification daemon is active and the notification
|
||||
// will be handled
|
||||
notifdService; // eslint-disable-line @typescript-eslint/no-unused-expressions
|
||||
|
||||
let command = 'notify-send';
|
||||
command += ` "${notifPayload.summary} "`;
|
||||
if (notifPayload.body !== undefined) command += ` "${notifPayload.body}" `;
|
||||
if (notifPayload.appName !== undefined) command += ` -a "${notifPayload.appName}"`;
|
||||
if (notifPayload.iconName !== undefined) command += ` -i "${notifPayload.iconName}"`;
|
||||
if (notifPayload.urgency !== undefined) command += ` -u "${notifPayload.urgency}"`;
|
||||
if (notifPayload.timeout !== undefined) command += ` -t ${notifPayload.timeout}`;
|
||||
if (notifPayload.category !== undefined) command += ` -c "${notifPayload.category}"`;
|
||||
if (notifPayload.transient !== undefined) 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: Gdk.Event): boolean => {
|
||||
const [directionSuccess, direction] = event.get_scroll_direction();
|
||||
const [deltaSuccess, , yScroll] = event.get_scroll_deltas();
|
||||
|
||||
if (directionSuccess && direction === Gdk.ScrollDirection.UP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deltaSuccess && yScroll < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: Gdk.Event): boolean => {
|
||||
const [directionSuccess, direction] = event.get_scroll_direction();
|
||||
const [deltaSuccess, , yScroll] = event.get_scroll_deltas();
|
||||
|
||||
if (directionSuccess && direction === Gdk.ScrollDirection.DOWN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (deltaSuccess && yScroll > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
34
src/lib/validation/colors.ts
Normal file
34
src/lib/validation/colors.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { namedColors } from './colorNames';
|
||||
import { HexColor } from '../options/types';
|
||||
|
||||
/**
|
||||
* Validates if a string is a valid GJS color
|
||||
* 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;
|
||||
}
|
||||
|
||||
export const isHexColor = (val: unknown): val is HexColor => {
|
||||
return typeof val === 'string' && /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(val);
|
||||
};
|
||||
18
src/lib/validation/images.ts
Normal file
18
src/lib/validation/images.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import GdkPixbuf from 'gi://GdkPixbuf';
|
||||
import { normalizeToAbsolutePath } from '../path/helpers';
|
||||
|
||||
/**
|
||||
* Checks if the provided filepath is a valid image
|
||||
* Note: Unlike GdkPixbuf, this function will normalize the given path
|
||||
* @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 {
|
||||
GdkPixbuf.Pixbuf.new_from_file(normalizeToAbsolutePath(imgFilePath));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.info(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
10
src/lib/validation/types.ts
Normal file
10
src/lib/validation/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type Primitive = string | number | boolean | symbol | null | undefined | bigint;
|
||||
|
||||
/**
|
||||
* Checks if a value is a primitive type
|
||||
* @param value - The value to check
|
||||
* @returns True if the value is a primitive (null, undefined, string, number, boolean, symbol, or bigint)
|
||||
*/
|
||||
export function isPrimitive(value: unknown): value is Primitive {
|
||||
return value === null || (typeof value !== 'object' && typeof value !== 'function');
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
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'),
|
||||
};
|
||||
22
src/lib/window/positioning.ts
Normal file
22
src/lib/window/positioning.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { NotificationAnchor, OSDAnchor, PositionAnchor } from 'src/lib/options/types';
|
||||
|
||||
/**
|
||||
* Maps a notification or OSD anchor position to an 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;
|
||||
}
|
||||
14
src/lib/window/visibility.ts
Normal file
14
src/lib/window/visibility.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { App } from 'astal/gtk3';
|
||||
import { Variable } from 'astal';
|
||||
|
||||
export function isWindowVisible(windowName: string): boolean {
|
||||
const appWindow = App.get_window(windowName);
|
||||
|
||||
if (appWindow === undefined || appWindow === null) {
|
||||
throw new Error(`Window with name "${windowName}" not found.`);
|
||||
}
|
||||
|
||||
return appWindow.visible;
|
||||
}
|
||||
|
||||
export const idleInhibit = Variable(false);
|
||||
Reference in New Issue
Block a user