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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
.weather.json
|
||||
node_modules
|
||||
prepare
|
||||
|
||||
@girs
|
||||
|
||||
**/.claude/settings.local.json
|
||||
|
||||
59
app.ts
59
app.ts
@@ -1,31 +1,27 @@
|
||||
import './src/lib/session';
|
||||
import './src/scss/style';
|
||||
import './src/shared/useTheme';
|
||||
import './src/shared/wallpaper';
|
||||
import './src/shared/systray';
|
||||
import './src/shared/dropdown';
|
||||
import './src/shared/utilities';
|
||||
import './src/components/bar/utils/sideEffects';
|
||||
|
||||
import './src/style';
|
||||
import 'src/core/behaviors/bar';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
const hyprland = AstalHyprland.get_default();
|
||||
|
||||
import { Bar } from './src/components/bar';
|
||||
import { DropdownMenus, StandardWindows } from './src/components/menus/exports';
|
||||
import Notifications from './src/components/notifications';
|
||||
import SettingsDialog from './src/components/settings/index';
|
||||
import { bash, forMonitors } from 'src/lib/utils';
|
||||
import options from 'src/options';
|
||||
import OSD from 'src/components/osd/index';
|
||||
import { App } from 'astal/gtk3';
|
||||
import { execAsync } from 'astal';
|
||||
import { handleRealization } from 'src/components/menus/shared/dropdown/helpers';
|
||||
import { isDropdownMenu } from 'src/lib/constants/options.js';
|
||||
import { initializeSystemBehaviors } from 'src/lib/behaviors';
|
||||
import { runCLI } from 'src/cli/commander';
|
||||
import { handleRealization } from 'src/components/menus/shared/dropdown/helpers/helpers';
|
||||
import { isDropdownMenu } from 'src/components/settings/constants.js';
|
||||
import { initializeSystemBehaviors } from 'src/core/behaviors';
|
||||
import { runCLI } from 'src/services/cli/commander';
|
||||
import { DropdownMenus, StandardWindows } from 'src/components/menus';
|
||||
import { forMonitors } from 'src/components/bar/utils/monitors';
|
||||
import options from 'src/configuration';
|
||||
import { SystemUtilities } from 'src/core/system/SystemUtilities';
|
||||
|
||||
const hyprland = AstalHyprland.get_default();
|
||||
const initializeStartupScripts = (): void => {
|
||||
execAsync(`python3 ${SRC_DIR}/scripts/bluetooth.py`).catch((err) => console.error(err));
|
||||
execAsync(`python3 ${SRC_DIR}/scripts/bluetooth.py`).catch((err) =>
|
||||
console.error('Failed to initialize bluetooth script:', err),
|
||||
);
|
||||
};
|
||||
|
||||
const initializeMenus = (): void => {
|
||||
@@ -38,7 +34,10 @@ const initializeMenus = (): void => {
|
||||
});
|
||||
|
||||
DropdownMenus.forEach((window) => {
|
||||
const windowName = window.name.replace('_default', '').concat('menu').toLowerCase();
|
||||
const windowName = window.name
|
||||
.replace(/_default.*/, '')
|
||||
.concat('menu')
|
||||
.toLowerCase();
|
||||
|
||||
if (!isDropdownMenu(windowName)) {
|
||||
return;
|
||||
@@ -54,18 +53,22 @@ App.start({
|
||||
runCLI(request, res);
|
||||
},
|
||||
async main() {
|
||||
initializeStartupScripts();
|
||||
try {
|
||||
initializeStartupScripts();
|
||||
|
||||
Notifications();
|
||||
OSD();
|
||||
Notifications();
|
||||
OSD();
|
||||
|
||||
const barsForMonitors = await forMonitors(Bar);
|
||||
barsForMonitors.forEach((bar: JSX.Element) => bar);
|
||||
const barsForMonitors = await forMonitors(Bar);
|
||||
barsForMonitors.forEach((bar: JSX.Element) => bar);
|
||||
|
||||
SettingsDialog();
|
||||
initializeMenus();
|
||||
SettingsDialog();
|
||||
initializeMenus();
|
||||
|
||||
initializeSystemBehaviors();
|
||||
initializeSystemBehaviors();
|
||||
} catch (error) {
|
||||
console.error('Error during application initialization:', error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,6 +76,6 @@ hyprland.connect('monitor-added', () => {
|
||||
const { restartCommand } = options.hyprpanel;
|
||||
|
||||
if (options.hyprpanel.restartAgs.get()) {
|
||||
bash(restartCommand.get());
|
||||
SystemUtilities.bash(restartCommand.get());
|
||||
}
|
||||
});
|
||||
|
||||
139
assets/tokyo-night.xml
Normal file
139
assets/tokyo-night.xml
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Tokyo Night color scheme for GtkSourceView
|
||||
Ported for HyprPanel
|
||||
-->
|
||||
|
||||
<style-scheme id="tokyo-night" name="Tokyo Night" version="1.0">
|
||||
<author>HyprPanel - Jas Singh</author>
|
||||
<description>Tokyo Night color scheme for GtkSourceView</description>
|
||||
|
||||
<!-- Global Settings -->
|
||||
<style name="text" foreground="#c0caf5" background="#1a1b26"/>
|
||||
<style name="selection" foreground="#c0caf5" background="#283457"/>
|
||||
<style name="cursor" foreground="#c0caf5"/>
|
||||
<style name="secondary-cursor" foreground="#c0caf5"/>
|
||||
<style name="current-line" background="#292e42"/>
|
||||
<style name="line-numbers" foreground="#565f89" background="#16161e"/>
|
||||
<style name="draw-spaces" foreground="#565f89"/>
|
||||
<style name="background-pattern" background="#292e42"/>
|
||||
<style name="bracket-match" foreground="#c0caf5" background="#3d59a1" bold="true"/>
|
||||
<style name="bracket-mismatch" foreground="#1a1b26" background="#f7768e" bold="true"/>
|
||||
<style name="right-margin" foreground="#565f89" background="#16161e"/>
|
||||
<style name="search-match" foreground="#1a1b26" background="#ff9e64"/>
|
||||
|
||||
<!-- Syntax Highlighting -->
|
||||
<!-- Comments -->
|
||||
<style name="def:comment" foreground="#565f89" italic="true"/>
|
||||
<style name="def:shebang" foreground="#565f89" bold="true"/>
|
||||
<style name="def:doc-comment-element" foreground="#565f89" italic="true"/>
|
||||
|
||||
<!-- Constants -->
|
||||
<style name="def:constant" foreground="#ff9e64"/>
|
||||
<style name="def:string" foreground="#9ece6a"/>
|
||||
<style name="def:special-char" foreground="#7dcfff"/>
|
||||
<style name="def:special-constant" foreground="#bb9af7"/>
|
||||
<style name="def:number" foreground="#ff9e64"/>
|
||||
<style name="def:floating-point" foreground="#ff9e64"/>
|
||||
<style name="def:decimal" foreground="#ff9e64"/>
|
||||
<style name="def:base-n-integer" foreground="#ff9e64"/>
|
||||
<style name="def:boolean" foreground="#bb9af7"/>
|
||||
<style name="def:character" foreground="#9ece6a"/>
|
||||
|
||||
<!-- Identifiers -->
|
||||
<style name="def:identifier" foreground="#c0caf5"/>
|
||||
<style name="def:function" foreground="#7aa2f7"/>
|
||||
<style name="def:builtin" foreground="#7dcfff"/>
|
||||
|
||||
<!-- Statements -->
|
||||
<style name="def:statement" foreground="#bb9af7"/>
|
||||
<style name="def:operator" foreground="#89ddff"/>
|
||||
<style name="def:keyword" foreground="#bb9af7" bold="true"/>
|
||||
<style name="def:type" foreground="#7dcfff"/>
|
||||
<style name="def:reserved" foreground="#bb9af7"/>
|
||||
|
||||
<!-- Types -->
|
||||
<style name="def:type" foreground="#7dcfff"/>
|
||||
|
||||
<!-- Others -->
|
||||
<style name="def:preprocessor" foreground="#bb9af7"/>
|
||||
<style name="def:error" foreground="#f7768e" underline="error"/>
|
||||
<style name="def:warning" foreground="#e0af68" underline="error"/>
|
||||
<style name="def:note" foreground="#7aa2f7" underline="error"/>
|
||||
<style name="def:net-address-in-comment" foreground="#7dcfff" underline="true"/>
|
||||
<style name="def:underlined" underline="single"/>
|
||||
|
||||
<!-- Language specific -->
|
||||
<!-- XML & HTML -->
|
||||
<style name="xml:attribute-name" foreground="#7aa2f7"/>
|
||||
<style name="xml:element-name" foreground="#f7768e"/>
|
||||
<style name="xml:entity" foreground="#bb9af7"/>
|
||||
<style name="xml:namespace" foreground="#f7768e" underline="true"/>
|
||||
<style name="xml:tag" foreground="#f7768e"/>
|
||||
<style name="xml:doctype" foreground="#bb9af7"/>
|
||||
<style name="xml:cdata-delim" foreground="#8c8c8c" bold="true"/>
|
||||
<style name="html:dtd" foreground="#bb9af7"/>
|
||||
<style name="html:tag" foreground="#f7768e"/>
|
||||
|
||||
<!-- CSS -->
|
||||
<style name="css:keyword" foreground="#7aa2f7"/>
|
||||
<style name="css:at-rules" foreground="#bb9af7"/>
|
||||
<style name="css:color" foreground="#ff9e64"/>
|
||||
<style name="css:string" foreground="#9ece6a"/>
|
||||
|
||||
<!-- Diff -->
|
||||
<style name="diff:added-line" foreground="#9ece6a"/>
|
||||
<style name="diff:removed-line" foreground="#f7768e"/>
|
||||
<style name="diff:changed-line" foreground="#e0af68"/>
|
||||
<style name="diff:special-case" foreground="#bb9af7"/>
|
||||
<style name="diff:location" foreground="#7aa2f7" bold="true"/>
|
||||
<style name="diff:diff-file" foreground="#e0af68" bold="true"/>
|
||||
|
||||
<!-- JSON specific -->
|
||||
<style name="json:keyname" foreground="#7aa2f7"/>
|
||||
<style name="json:special-char" foreground="#89ddff"/>
|
||||
<style name="json:string" foreground="#9ece6a"/>
|
||||
<style name="json:boolean" foreground="#ff9e64"/>
|
||||
<style name="json:null-value" foreground="#bb9af7"/>
|
||||
<style name="json:float" foreground="#ff9e64"/>
|
||||
<style name="json:decimal" foreground="#ff9e64"/>
|
||||
<style name="json:error" foreground="#f7768e" underline="error"/>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<style name="js:function" foreground="#7aa2f7"/>
|
||||
<style name="js:string" foreground="#9ece6a"/>
|
||||
<style name="js:regex" foreground="#7dcfff"/>
|
||||
|
||||
<!-- Python -->
|
||||
<style name="python:builtin-constant" foreground="#bb9af7"/>
|
||||
<style name="python:builtin-function" foreground="#7aa2f7"/>
|
||||
<style name="python:module-handler" foreground="#bb9af7"/>
|
||||
<style name="python:special-variable" foreground="#bb9af7"/>
|
||||
<style name="python:string-conversion" foreground="#7dcfff"/>
|
||||
<style name="python:format" foreground="#7dcfff"/>
|
||||
<style name="python:decorator" foreground="#bb9af7"/>
|
||||
|
||||
<!-- C/C++ -->
|
||||
<style name="c:preprocessor" foreground="#bb9af7"/>
|
||||
<style name="c:common-defines" foreground="#bb9af7"/>
|
||||
<style name="c:included-file" foreground="#9ece6a"/>
|
||||
<style name="c:char" foreground="#9ece6a"/>
|
||||
|
||||
<!-- Markdown -->
|
||||
<style name="markdown:header" foreground="#f7768e" bold="true"/>
|
||||
<style name="markdown:list-marker" foreground="#ff9e64" bold="true"/>
|
||||
<style name="markdown:code" foreground="#9ece6a"/>
|
||||
<style name="markdown:emphasis" foreground="#e0af68" italic="true"/>
|
||||
<style name="markdown:strong-emphasis" foreground="#e0af68" bold="true"/>
|
||||
<style name="markdown:url" foreground="#7aa2f7" underline="true"/>
|
||||
<style name="markdown:link-text" foreground="#bb9af7"/>
|
||||
<style name="markdown:backslash-escape" foreground="#ff9e64"/>
|
||||
<style name="markdown:line-break" foreground="#565f89"/>
|
||||
|
||||
<!-- Others -->
|
||||
<style name="def:variable" foreground="#c0caf5"/>
|
||||
<style name="def:class" foreground="#7aa2f7" bold="true"/>
|
||||
<style name="def:interface" foreground="#7aa2f7" italic="true"/>
|
||||
<style name="def:method" foreground="#7aa2f7"/>
|
||||
<style name="def:namespace" foreground="#bb9af7" underline="true"/>
|
||||
</style-scheme>
|
||||
@@ -41,4 +41,4 @@ configure_file(
|
||||
install_subdir('scripts', install_dir: datadir)
|
||||
install_subdir('themes', install_dir: datadir)
|
||||
install_subdir('assets', install_dir: datadir)
|
||||
install_subdir('src/scss', install_dir: datadir / 'src')
|
||||
install_subdir('src/style', install_dir: datadir / 'src')
|
||||
|
||||
916
package-lock.json
generated
916
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"lint": "eslint --config .eslintrc.json .",
|
||||
"lint:fix": "eslint --config .eslintrc.json . --fix",
|
||||
"format": "prettier --write 'modules/**/*.ts'"
|
||||
"format": "prettier --write 'modules/**/*.ts'",
|
||||
"knip": "knip"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -15,13 +16,14 @@
|
||||
"astal": "/usr/share/astal/gjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/node": "^22.15.17",
|
||||
"@typescript-eslint/eslint-plugin": "^8.5.0",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"knip": "^5.55.1",
|
||||
"prettier": "^3.3.3",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "5.7.3"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BarToggleStates } from 'src/lib/types/cli.types';
|
||||
|
||||
export class BarVisibility {
|
||||
private static _toggleStates: BarToggleStates = {};
|
||||
|
||||
public static get(barName: string): boolean {
|
||||
return this._toggleStates[barName] ?? true;
|
||||
}
|
||||
|
||||
public static set(barName: string, isVisible: boolean): void {
|
||||
this._toggleStates[barName] = isVisible;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Gio, readFileAsync } from 'astal';
|
||||
import { CustomBarModule } from './types';
|
||||
import { CustomBarModule, WidgetMap } from './types';
|
||||
import { ModuleContainer } from './module_container';
|
||||
import { WidgetContainer } from '../shared/WidgetContainer';
|
||||
import { WidgetMap } from '..';
|
||||
import { WidgetContainer } from '../shared/widgetContainer';
|
||||
|
||||
export class CustomModules {
|
||||
constructor() {}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isPrimitive } from 'src/lib/utils';
|
||||
import { isPrimitive } from 'src/lib/validation/types';
|
||||
import { CustomBarModuleIcon } from '../../types';
|
||||
import { parseCommandOutputJson } from './utils';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isPrimitive } from 'src/lib/utils';
|
||||
import { isPrimitive } from 'src/lib/validation/types';
|
||||
|
||||
/**
|
||||
* Generates a label based on module command output and a template configuration.
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CustomBarModule } from '../types';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { Module } from '../../shared/module';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { getIcon } from './helpers/icon';
|
||||
import { getLabel } from './helpers/label';
|
||||
import { initActionListener, initCommandPoller, setupModuleInteractions } from './setup';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
|
||||
export const ModuleContainer = (moduleName: string, moduleMetadata: CustomBarModule): BarBoxChild => {
|
||||
const {
|
||||
@@ -2,7 +2,9 @@ import { Variable, bind, execAsync } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BashPoller } from 'src/lib/poller/BashPoller';
|
||||
import { CustomBarModule } from '../types';
|
||||
import { inputHandler } from '../../utils/helpers';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
export function initCommandPoller(
|
||||
commandOutput: Variable<string>,
|
||||
@@ -51,7 +53,7 @@ export function setupModuleInteractions(
|
||||
moduleScrollThreshold: number,
|
||||
): void {
|
||||
const scrollThreshold = moduleScrollThreshold >= 0 ? moduleScrollThreshold : 1;
|
||||
inputHandler(
|
||||
inputHandler.attachHandlers(
|
||||
element,
|
||||
{
|
||||
onPrimaryClick: {
|
||||
@@ -1,4 +1,4 @@
|
||||
export type CustomBarModuleActions = {
|
||||
type CustomBarModuleActions = {
|
||||
onLeftClick?: string;
|
||||
onRightClick?: string;
|
||||
onMiddleClick?: string;
|
||||
@@ -18,3 +18,7 @@ export type CustomBarModule = {
|
||||
actions?: CustomBarModuleActions;
|
||||
};
|
||||
export type CustomBarModuleIcon = string | string[] | Record<string, string>;
|
||||
|
||||
export type WidgetMap = {
|
||||
[key in string]: (monitor: number) => JSX.Element;
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Menu } from './modules/menu';
|
||||
import { Workspaces } from '../../components/bar/modules/workspaces/index';
|
||||
import { ClientTitle } from '../../components/bar/modules/window_title/index';
|
||||
import { Media } from '../../components/bar/modules/media/index';
|
||||
import { Notifications } from '../../components/bar/modules/notifications/index';
|
||||
import { Volume } from '../../components/bar/modules/volume/index';
|
||||
import { Network } from '../../components/bar/modules/network/index';
|
||||
import { Bluetooth } from '../../components/bar/modules/bluetooth/index';
|
||||
import { BatteryLabel } from '../../components/bar/modules/battery/index';
|
||||
import { Clock } from '../../components/bar/modules/clock/index';
|
||||
import { SysTray } from '../../components/bar/modules/systray/index';
|
||||
|
||||
// Basic Modules
|
||||
import { Microphone } from '../../components/bar/modules/microphone/index';
|
||||
import { Ram } from '../../components/bar/modules/ram/index';
|
||||
import { Cpu } from '../../components/bar/modules/cpu/index';
|
||||
import { CpuTemp } from '../../components/bar/modules/cputemp/index';
|
||||
import { Storage } from '../../components/bar/modules/storage/index';
|
||||
import { Netstat } from '../../components/bar/modules/netstat/index';
|
||||
import { KbInput } from '../../components/bar/modules/kblayout/index';
|
||||
import { Updates } from '../../components/bar/modules/updates/index';
|
||||
import { Submap } from '../../components/bar/modules/submap/index';
|
||||
import { Weather } from '../../components/bar/modules/weather/index';
|
||||
import { Power } from '../../components/bar/modules/power/index';
|
||||
import { Hyprsunset } from '../../components/bar/modules/hyprsunset/index';
|
||||
import { Hypridle } from '../../components/bar/modules/hypridle/index';
|
||||
import { Cava } from '../../components/bar/modules/cava/index';
|
||||
import { WorldClock } from '../../components/bar/modules/worldclock/index';
|
||||
|
||||
import { ModuleSeparator } from './modules/separator';
|
||||
|
||||
export {
|
||||
Menu,
|
||||
Workspaces,
|
||||
ClientTitle,
|
||||
Media,
|
||||
Notifications,
|
||||
Volume,
|
||||
Network,
|
||||
Bluetooth,
|
||||
BatteryLabel,
|
||||
Clock,
|
||||
SysTray,
|
||||
|
||||
// Basic Modules
|
||||
Microphone,
|
||||
Ram,
|
||||
Cpu,
|
||||
CpuTemp,
|
||||
Storage,
|
||||
Netstat,
|
||||
KbInput,
|
||||
Updates,
|
||||
Submap,
|
||||
Weather,
|
||||
Power,
|
||||
Hyprsunset,
|
||||
Hypridle,
|
||||
Cava,
|
||||
WorldClock,
|
||||
ModuleSeparator,
|
||||
};
|
||||
@@ -1,198 +1,19 @@
|
||||
import {
|
||||
Menu,
|
||||
Workspaces,
|
||||
ClientTitle,
|
||||
Media,
|
||||
Notifications,
|
||||
Volume,
|
||||
Network,
|
||||
Bluetooth,
|
||||
BatteryLabel,
|
||||
Clock,
|
||||
SysTray,
|
||||
Microphone,
|
||||
Ram,
|
||||
Cpu,
|
||||
CpuTemp,
|
||||
Storage,
|
||||
Netstat,
|
||||
KbInput,
|
||||
Updates,
|
||||
Submap,
|
||||
Weather,
|
||||
Power,
|
||||
Hyprsunset,
|
||||
Hypridle,
|
||||
Cava,
|
||||
WorldClock,
|
||||
ModuleSeparator,
|
||||
} from './exports';
|
||||
import { GdkMonitorService } from 'src/services/display/monitor';
|
||||
import { BarLayout } from './layout/BarLayout';
|
||||
import { getCoreWidgets } from './layout/coreWidgets';
|
||||
import { WidgetRegistry } from './layout/WidgetRegistry';
|
||||
|
||||
import { WidgetContainer } from './shared/WidgetContainer';
|
||||
import options from 'src/options';
|
||||
import { App, Gtk } from 'astal/gtk3';
|
||||
|
||||
import Astal from 'gi://Astal?version=3.0';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { getLayoutForMonitor, isLayoutEmpty } from './utils/monitors';
|
||||
import { GdkMonitorMapper } from './utils/GdkMonitorMapper';
|
||||
import { CustomModules } from './custom_modules/CustomModules';
|
||||
import { idleInhibit } from 'src/shared/utilities';
|
||||
|
||||
const { layouts } = options.bar;
|
||||
const { location } = options.theme.bar;
|
||||
const { location: borderLocation } = options.theme.bar.border;
|
||||
|
||||
let widgets: WidgetMap = {
|
||||
battery: () => WidgetContainer(BatteryLabel()),
|
||||
dashboard: () => WidgetContainer(Menu()),
|
||||
workspaces: (monitor: number) => WidgetContainer(Workspaces(monitor)),
|
||||
windowtitle: () => WidgetContainer(ClientTitle()),
|
||||
media: () => WidgetContainer(Media()),
|
||||
notifications: () => WidgetContainer(Notifications()),
|
||||
volume: () => WidgetContainer(Volume()),
|
||||
network: () => WidgetContainer(Network()),
|
||||
bluetooth: () => WidgetContainer(Bluetooth()),
|
||||
clock: () => WidgetContainer(Clock()),
|
||||
systray: () => WidgetContainer(SysTray()),
|
||||
microphone: () => WidgetContainer(Microphone()),
|
||||
ram: () => WidgetContainer(Ram()),
|
||||
cpu: () => WidgetContainer(Cpu()),
|
||||
cputemp: () => WidgetContainer(CpuTemp()),
|
||||
storage: () => WidgetContainer(Storage()),
|
||||
netstat: () => WidgetContainer(Netstat()),
|
||||
kbinput: () => WidgetContainer(KbInput()),
|
||||
updates: () => WidgetContainer(Updates()),
|
||||
submap: () => WidgetContainer(Submap()),
|
||||
weather: () => WidgetContainer(Weather()),
|
||||
power: () => WidgetContainer(Power()),
|
||||
hyprsunset: () => WidgetContainer(Hyprsunset()),
|
||||
hypridle: () => WidgetContainer(Hypridle()),
|
||||
cava: () => WidgetContainer(Cava()),
|
||||
worldclock: () => WidgetContainer(WorldClock()),
|
||||
separator: () => ModuleSeparator(),
|
||||
};
|
||||
|
||||
const gdkMonitorMapper = new GdkMonitorMapper();
|
||||
const gdkMonitorService = new GdkMonitorService();
|
||||
const widgetRegistry = new WidgetRegistry(getCoreWidgets());
|
||||
|
||||
/**
|
||||
* Factory function to create a Bar for a specific monitor
|
||||
*/
|
||||
export const Bar = async (monitor: number): Promise<JSX.Element> => {
|
||||
try {
|
||||
const customWidgets = await CustomModules.build();
|
||||
widgets = {
|
||||
...widgets,
|
||||
...customWidgets,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
const hyprlandMonitor = gdkMonitorMapper.mapGdkToHyprland(monitor);
|
||||
await widgetRegistry.initialize();
|
||||
|
||||
const computeVisibility = bind(layouts).as(() => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get());
|
||||
return !isLayoutEmpty(foundLayout);
|
||||
});
|
||||
const hyprlandMonitor = gdkMonitorService.mapGdkToHyprland(monitor);
|
||||
const barLayout = new BarLayout(monitor, hyprlandMonitor, widgetRegistry);
|
||||
|
||||
const computeClassName = bind(layouts).as(() => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get());
|
||||
return !isLayoutEmpty(foundLayout) ? 'bar' : '';
|
||||
});
|
||||
|
||||
const computeAnchor = bind(location).as((loc) => {
|
||||
if (loc === 'bottom') {
|
||||
return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||
}
|
||||
|
||||
return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||
});
|
||||
|
||||
const computeLayer = Variable.derive(
|
||||
[bind(options.theme.bar.layer), bind(options.tear)],
|
||||
(barLayer, tear) => {
|
||||
if (tear && barLayer === 'overlay') {
|
||||
return Astal.Layer.TOP;
|
||||
}
|
||||
const layerMap = {
|
||||
overlay: Astal.Layer.OVERLAY,
|
||||
top: Astal.Layer.TOP,
|
||||
bottom: Astal.Layer.BOTTOM,
|
||||
background: Astal.Layer.BACKGROUND,
|
||||
};
|
||||
|
||||
return layerMap[barLayer];
|
||||
},
|
||||
);
|
||||
|
||||
const computeBorderLocation = bind(borderLocation).as((brdrLcn) =>
|
||||
brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel',
|
||||
);
|
||||
|
||||
const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||
|
||||
return foundLayout.left
|
||||
.filter((mod) => Object.keys(widgets).includes(mod))
|
||||
.map((w) => widgets[w](hyprlandMonitor));
|
||||
});
|
||||
const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||
|
||||
return foundLayout.middle
|
||||
.filter((mod) => Object.keys(widgets).includes(mod))
|
||||
.map((w) => widgets[w](hyprlandMonitor));
|
||||
});
|
||||
const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts);
|
||||
|
||||
return foundLayout.right
|
||||
.filter((mod) => Object.keys(widgets).includes(mod))
|
||||
.map((w) => widgets[w](hyprlandMonitor));
|
||||
});
|
||||
|
||||
return (
|
||||
<window
|
||||
inhibit={bind(idleInhibit)}
|
||||
name={`bar-${hyprlandMonitor}`}
|
||||
namespace={`bar-${hyprlandMonitor}`}
|
||||
className={computeClassName}
|
||||
application={App}
|
||||
monitor={monitor}
|
||||
visible={computeVisibility}
|
||||
anchor={computeAnchor}
|
||||
layer={computeLayer()}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||
onDestroy={() => {
|
||||
computeLayer.drop();
|
||||
leftBinding.drop();
|
||||
middleBinding.drop();
|
||||
rightBinding.drop();
|
||||
}}
|
||||
>
|
||||
<box className={'bar-panel-container'}>
|
||||
<centerbox
|
||||
css={'padding: 1px;'}
|
||||
hexpand
|
||||
className={computeBorderLocation}
|
||||
startWidget={
|
||||
<box className={'box-left'} hexpand>
|
||||
{leftBinding()}
|
||||
</box>
|
||||
}
|
||||
centerWidget={
|
||||
<box className={'box-center'} halign={Gtk.Align.CENTER}>
|
||||
{middleBinding()}
|
||||
</box>
|
||||
}
|
||||
endWidget={
|
||||
<box className={'box-right'} halign={Gtk.Align.END}>
|
||||
{rightBinding()}
|
||||
</box>
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
</window>
|
||||
);
|
||||
};
|
||||
|
||||
export type WidgetMap = {
|
||||
[K in string]: (monitor: number) => JSX.Element;
|
||||
return barLayout.render();
|
||||
};
|
||||
|
||||
185
src/components/bar/layout/BarLayout.tsx
Normal file
185
src/components/bar/layout/BarLayout.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { App, Gtk } from 'astal/gtk3';
|
||||
import Astal from 'gi://Astal?version=3.0';
|
||||
import { bind, Binding, Variable } from 'astal';
|
||||
import { idleInhibit } from 'src/lib/window/visibility';
|
||||
import { WidgetRegistry } from './WidgetRegistry';
|
||||
import { getLayoutForMonitor, isLayoutEmpty } from '../utils/monitors';
|
||||
import options from 'src/configuration';
|
||||
|
||||
/**
|
||||
* Responsible for the bar UI layout and positioning
|
||||
*/
|
||||
export class BarLayout {
|
||||
private _hyprlandMonitor: number;
|
||||
private _gdkMonitor: number;
|
||||
private _widgetRegistry: WidgetRegistry;
|
||||
|
||||
private _visibilityVar: Variable<boolean>;
|
||||
private _classNameVar: Variable<string>;
|
||||
private _anchorVar: Variable<Astal.WindowAnchor>;
|
||||
private _layerVar: Variable<Astal.Layer>;
|
||||
private _borderLocationVar: Binding<string>;
|
||||
private _barSectionsVar: {
|
||||
left: Variable<JSX.Element[]>;
|
||||
middle: Variable<JSX.Element[]>;
|
||||
right: Variable<JSX.Element[]>;
|
||||
};
|
||||
|
||||
constructor(gdkMonitor: number, hyprlandMonitor: number, widgetRegistry: WidgetRegistry) {
|
||||
this._gdkMonitor = gdkMonitor;
|
||||
this._hyprlandMonitor = hyprlandMonitor;
|
||||
this._widgetRegistry = widgetRegistry;
|
||||
|
||||
this._visibilityVar = Variable(true);
|
||||
this._classNameVar = Variable('bar');
|
||||
this._anchorVar = Variable(
|
||||
Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT,
|
||||
);
|
||||
this._layerVar = Variable(Astal.Layer.TOP);
|
||||
this._borderLocationVar = Variable('bar-panel')();
|
||||
this._barSectionsVar = {
|
||||
left: Variable([]),
|
||||
middle: Variable([]),
|
||||
right: Variable([]),
|
||||
};
|
||||
|
||||
this._initializeReactiveVariables();
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<window
|
||||
inhibit={bind(idleInhibit)}
|
||||
name={`bar-${this._hyprlandMonitor}`}
|
||||
namespace={`bar-${this._hyprlandMonitor}`}
|
||||
className={this._classNameVar()}
|
||||
application={App}
|
||||
monitor={this._gdkMonitor}
|
||||
visible={this._visibilityVar()}
|
||||
anchor={this._anchorVar()}
|
||||
layer={this._layerVar()}
|
||||
exclusivity={Astal.Exclusivity.EXCLUSIVE}
|
||||
onDestroy={() => this._cleanup()}
|
||||
>
|
||||
<box className="bar-panel-container">
|
||||
<centerbox
|
||||
css="padding: 1px;"
|
||||
hexpand
|
||||
className={this._borderLocationVar}
|
||||
startWidget={
|
||||
<box className="box-left" hexpand>
|
||||
{this._barSectionsVar.left()}
|
||||
</box>
|
||||
}
|
||||
centerWidget={
|
||||
<box className="box-center" halign={Gtk.Align.CENTER}>
|
||||
{this._barSectionsVar.middle()}
|
||||
</box>
|
||||
}
|
||||
endWidget={
|
||||
<box className="box-right" halign={Gtk.Align.END}>
|
||||
{this._barSectionsVar.right()}
|
||||
</box>
|
||||
}
|
||||
/>
|
||||
</box>
|
||||
</window>
|
||||
);
|
||||
}
|
||||
|
||||
private _initializeReactiveVariables(): void {
|
||||
this._initializeVisibilityVariables();
|
||||
this._initializePositionVariables();
|
||||
this._initializeAppearanceVariables();
|
||||
this._initializeSectionVariables();
|
||||
}
|
||||
|
||||
private _initializeVisibilityVariables(): void {
|
||||
const { layouts } = options.bar;
|
||||
|
||||
this._visibilityVar = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(this._hyprlandMonitor, currentLayouts);
|
||||
return !isLayoutEmpty(foundLayout);
|
||||
});
|
||||
|
||||
this._classNameVar = Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(this._hyprlandMonitor, currentLayouts);
|
||||
return !isLayoutEmpty(foundLayout) ? 'bar' : '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize variables related to bar positioning
|
||||
*/
|
||||
private _initializePositionVariables(): void {
|
||||
const { location } = options.theme.bar;
|
||||
|
||||
this._anchorVar = Variable.derive([bind(location)], (loc) => {
|
||||
if (loc === 'bottom') {
|
||||
return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||
}
|
||||
return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT;
|
||||
});
|
||||
}
|
||||
|
||||
private _initializeAppearanceVariables(): void {
|
||||
const { location: borderLocation } = options.theme.bar.border;
|
||||
|
||||
this._layerVar = this._createLayerVariable();
|
||||
|
||||
this._borderLocationVar = bind(borderLocation).as((brdrLcn) =>
|
||||
brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel',
|
||||
);
|
||||
}
|
||||
|
||||
private _createLayerVariable(): Variable<Astal.Layer> {
|
||||
return Variable.derive([bind(options.theme.bar.layer), bind(options.tear)], (barLayer, tear) => {
|
||||
if (tear && barLayer === 'overlay') {
|
||||
return Astal.Layer.TOP;
|
||||
}
|
||||
|
||||
return this._getLayerFromConfig(barLayer);
|
||||
});
|
||||
}
|
||||
|
||||
private _getLayerFromConfig(barLayer: string): Astal.Layer {
|
||||
const layerMap: Record<string, Astal.Layer> = {
|
||||
overlay: Astal.Layer.OVERLAY,
|
||||
top: Astal.Layer.TOP,
|
||||
bottom: Astal.Layer.BOTTOM,
|
||||
background: Astal.Layer.BACKGROUND,
|
||||
};
|
||||
|
||||
return layerMap[barLayer] ?? Astal.Layer.TOP;
|
||||
}
|
||||
|
||||
private _initializeSectionVariables(): void {
|
||||
this._barSectionsVar = {
|
||||
left: this._createSectionBinding('left'),
|
||||
middle: this._createSectionBinding('middle'),
|
||||
right: this._createSectionBinding('right'),
|
||||
};
|
||||
}
|
||||
|
||||
private _createSectionBinding(section: 'left' | 'middle' | 'right'): Variable<JSX.Element[]> {
|
||||
const { layouts } = options.bar;
|
||||
|
||||
return Variable.derive([bind(layouts)], (currentLayouts) => {
|
||||
const foundLayout = getLayoutForMonitor(this._hyprlandMonitor, currentLayouts);
|
||||
return foundLayout[section]
|
||||
.filter((mod) => this._widgetRegistry.hasWidget(mod))
|
||||
.map((widget) => this._widgetRegistry.createWidget(widget, this._hyprlandMonitor));
|
||||
});
|
||||
}
|
||||
|
||||
private _cleanup(): void {
|
||||
this._visibilityVar.drop();
|
||||
this._classNameVar.drop();
|
||||
this._anchorVar.drop();
|
||||
this._layerVar.drop();
|
||||
|
||||
this._barSectionsVar.left.drop();
|
||||
this._barSectionsVar.middle.drop();
|
||||
this._barSectionsVar.right.drop();
|
||||
}
|
||||
}
|
||||
55
src/components/bar/layout/WidgetRegistry.tsx
Normal file
55
src/components/bar/layout/WidgetRegistry.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { CustomModules } from '../customModules';
|
||||
|
||||
export type WidgetFactory = (monitor: number) => JSX.Element;
|
||||
|
||||
/**
|
||||
* Manages registration and creation of widgets
|
||||
*/
|
||||
export class WidgetRegistry {
|
||||
private _widgets: Record<string, WidgetFactory> = {};
|
||||
private _initialized = false;
|
||||
|
||||
constructor(coreWidgets: Record<string, WidgetFactory>) {
|
||||
this._widgets = { ...coreWidgets };
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the registry with core and custom widgets
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
try {
|
||||
const customWidgets = await CustomModules.build();
|
||||
|
||||
this._widgets = {
|
||||
...this._widgets,
|
||||
...customWidgets,
|
||||
};
|
||||
|
||||
this._initialized = true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize widget registry:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a widget is registered
|
||||
*/
|
||||
public hasWidget(name: string): boolean {
|
||||
return Object.keys(this._widgets).includes(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of a widget
|
||||
*/
|
||||
public createWidget(name: string, monitor: number): JSX.Element {
|
||||
if (!this.hasWidget(name)) {
|
||||
console.error(`Widget "${name}" not found`);
|
||||
return <box />;
|
||||
}
|
||||
|
||||
return this._widgets[name](monitor);
|
||||
}
|
||||
}
|
||||
61
src/components/bar/layout/coreWidgets.tsx
Normal file
61
src/components/bar/layout/coreWidgets.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { BatteryLabel } from '../modules/battery';
|
||||
import { Bluetooth } from '../modules/bluetooth';
|
||||
import { Cava } from '../modules/cava';
|
||||
import { Clock } from '../modules/clock';
|
||||
import { Cpu } from '../modules/cpu';
|
||||
import { CpuTemp } from '../modules/cputemp';
|
||||
import { Hypridle } from '../modules/hypridle';
|
||||
import { Hyprsunset } from '../modules/hyprsunset';
|
||||
import { KbInput } from '../modules/kblayout';
|
||||
import { Media } from '../modules/media';
|
||||
import { Menu } from '../modules/menu';
|
||||
import { Microphone } from '../modules/microphone';
|
||||
import { Netstat } from '../modules/netstat';
|
||||
import { Network } from '../modules/network';
|
||||
import { Notifications } from '../modules/notifications';
|
||||
import { Power } from '../modules/power';
|
||||
import { Ram } from '../modules/ram';
|
||||
import { ModuleSeparator } from '../modules/separator';
|
||||
import { Storage } from '../modules/storage';
|
||||
import { Submap } from '../modules/submap';
|
||||
import { SysTray } from '../modules/systray';
|
||||
import { Updates } from '../modules/updates';
|
||||
import { Volume } from '../modules/volume';
|
||||
import { Weather } from '../modules/weather';
|
||||
import { ClientTitle } from '../modules/window_title';
|
||||
import { Workspaces } from '../modules/workspaces';
|
||||
import { WorldClock } from '../modules/worldclock';
|
||||
import { WidgetContainer } from '../shared/widgetContainer';
|
||||
import { WidgetFactory } from './WidgetRegistry';
|
||||
|
||||
export function getCoreWidgets(): Record<string, WidgetFactory> {
|
||||
return {
|
||||
battery: () => WidgetContainer(BatteryLabel()),
|
||||
dashboard: () => WidgetContainer(Menu()),
|
||||
workspaces: (monitor: number) => WidgetContainer(Workspaces(monitor)),
|
||||
windowtitle: () => WidgetContainer(ClientTitle()),
|
||||
media: () => WidgetContainer(Media()),
|
||||
notifications: () => WidgetContainer(Notifications()),
|
||||
volume: () => WidgetContainer(Volume()),
|
||||
network: () => WidgetContainer(Network()),
|
||||
bluetooth: () => WidgetContainer(Bluetooth()),
|
||||
clock: () => WidgetContainer(Clock()),
|
||||
systray: () => WidgetContainer(SysTray()),
|
||||
microphone: () => WidgetContainer(Microphone()),
|
||||
ram: () => WidgetContainer(Ram()),
|
||||
cpu: () => WidgetContainer(Cpu()),
|
||||
cputemp: () => WidgetContainer(CpuTemp()),
|
||||
storage: () => WidgetContainer(Storage()),
|
||||
netstat: () => WidgetContainer(Netstat()),
|
||||
kbinput: () => WidgetContainer(KbInput()),
|
||||
updates: () => WidgetContainer(Updates()),
|
||||
submap: () => WidgetContainer(Submap()),
|
||||
weather: () => WidgetContainer(Weather()),
|
||||
power: () => WidgetContainer(Power()),
|
||||
hyprsunset: () => WidgetContainer(Hyprsunset()),
|
||||
hypridle: () => WidgetContainer(Hypridle()),
|
||||
cava: () => WidgetContainer(Cava()),
|
||||
worldclock: () => WidgetContainer(WorldClock()),
|
||||
separator: () => ModuleSeparator(),
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BatteryIconKeys, BatteryIcons } from 'src/lib/types/battery.types';
|
||||
import { BatteryIcons, BatteryIconKeys } from './types';
|
||||
|
||||
const batteryIcons: BatteryIcons = {
|
||||
0: '',
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import AstalBattery from 'gi://AstalBattery?version=0.1';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import options from 'src/options';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
import Variable from 'astal/variable';
|
||||
import { bind } from 'astal';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { getBatteryIcon } from './helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
|
||||
const batteryService = AstalBattery.get_default();
|
||||
|
||||
const {
|
||||
label: show_label,
|
||||
rightClick,
|
||||
@@ -136,7 +138,7 @@ const BatteryLabel = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'energymenu');
|
||||
openDropdownMenu(clicked, event, 'energymenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import options from 'src/options.js';
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { Variable, bind } from 'astal';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types.js';
|
||||
import { BarBoxChild } from 'src/components/bar/types.js';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
import { onPrimaryClick, onSecondaryClick, onMiddleClick, onScroll } from 'src/lib/shared/eventHandlers';
|
||||
|
||||
const bluetoothService = AstalBluetooth.get_default();
|
||||
|
||||
@@ -90,7 +91,7 @@ const Bluetooth = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'bluetoothmenu');
|
||||
openDropdownMenu(clicked, event, 'bluetoothmenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import AstalCava from 'gi://AstalCava?version=0.1';
|
||||
import AstalMpris from 'gi://AstalMpris?version=0.1';
|
||||
import options from 'src/options';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const mprisService = AstalMpris.get_default();
|
||||
const {
|
||||
showActiveOnly,
|
||||
bars,
|
||||
@@ -24,6 +23,7 @@ const {
|
||||
*/
|
||||
export function initVisibilityTracker(isVis: Variable<boolean>): Variable<void> {
|
||||
const cavaService = AstalCava.get_default();
|
||||
const mprisService = AstalMpris.get_default();
|
||||
|
||||
return Variable.derive([bind(showActiveOnly), bind(mprisService, 'players')], (showActive, players) => {
|
||||
isVis.set(cavaService !== null && (!showActive || players?.length > 0));
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Variable, bind } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from '../../utils/helpers';
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/module';
|
||||
import { initSettingsTracker, initVisibilityTracker } from './helpers';
|
||||
import AstalCava from 'gi://AstalCava?version=0.1';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const {
|
||||
icon,
|
||||
@@ -45,6 +47,8 @@ export const Cava = (): BarBoxChild => {
|
||||
);
|
||||
}
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
return Module({
|
||||
isVis: bind(isVis),
|
||||
label: labelBinding(),
|
||||
@@ -53,7 +57,7 @@ export const Cava = (): BarBoxChild => {
|
||||
boxClass: 'cava',
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -72,9 +76,10 @@ export const Cava = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
settingsTracker?.drop();
|
||||
labelBinding.drop();
|
||||
visTracker.drop();
|
||||
settingsTracker?.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import options from 'src/options';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { systemTime } from 'src/shared/time';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { systemTime } from 'src/lib/units/time';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
|
||||
const { format, icon, showIcon, showTime, rightClick, middleClick, scrollUp, scrollDown } = options.bar.clock;
|
||||
const { style } = options.theme.bar.buttons;
|
||||
@@ -83,7 +84,7 @@ const Clock = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'calendarmenu');
|
||||
openDropdownMenu(clicked, event, 'calendarmenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import GTop from 'gi://GTop';
|
||||
|
||||
let previousCpuData = new GTop.glibtop_cpu();
|
||||
GTop.glibtop_get_cpu(previousCpuData);
|
||||
|
||||
/**
|
||||
* Computes the CPU usage percentage.
|
||||
*
|
||||
* This function calculates the CPU usage percentage by comparing the current CPU data with the previous CPU data.
|
||||
* It calculates the differences in total and idle CPU times and uses these differences to compute the usage percentage.
|
||||
*
|
||||
* @returns The CPU usage percentage as a number.
|
||||
*/
|
||||
export const computeCPU = (): number => {
|
||||
const currentCpuData = new GTop.glibtop_cpu();
|
||||
GTop.glibtop_get_cpu(currentCpuData);
|
||||
|
||||
const totalDiff = currentCpuData.total - previousCpuData.total;
|
||||
const idleDiff = currentCpuData.idle - previousCpuData.idle;
|
||||
|
||||
const cpuUsagePercentage = totalDiff > 0 ? ((totalDiff - idleDiff) / totalDiff) * 100 : 0;
|
||||
|
||||
previousCpuData = currentCpuData;
|
||||
|
||||
return cpuUsagePercentage;
|
||||
};
|
||||
@@ -1,25 +1,29 @@
|
||||
import { Module } from '../../shared/Module';
|
||||
import options from 'src/options';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { computeCPU } from './helpers';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { Module } from '../../shared/module';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import CpuUsageService from 'src/services/system/cpuUsage';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const { label, round, leftClick, rightClick, middleClick, scrollUp, scrollDown, pollingInterval, icon } =
|
||||
options.bar.customModules.cpu;
|
||||
|
||||
export const cpuUsage = Variable(0);
|
||||
|
||||
const cpuPoller = new FunctionPoller<number, []>(cpuUsage, [bind(round)], bind(pollingInterval), computeCPU);
|
||||
|
||||
cpuPoller.initialize('cpu');
|
||||
const cpuService = new CpuUsageService({ frequency: pollingInterval });
|
||||
|
||||
export const Cpu = (): BarBoxChild => {
|
||||
const labelBinding = Variable.derive([bind(cpuUsage), bind(round)], (cpuUsg: number, round: boolean) => {
|
||||
return round ? `${Math.round(cpuUsg)}%` : `${cpuUsg.toFixed(2)}%`;
|
||||
});
|
||||
cpuService.initialize();
|
||||
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(cpuService.cpu), bind(round)],
|
||||
(cpuUsg: number, round: boolean) => {
|
||||
return round ? `${Math.round(cpuUsg)}%` : `${cpuUsg.toFixed(2)}%`;
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const cpuModule = Module({
|
||||
textIcon: bind(icon),
|
||||
@@ -29,7 +33,7 @@ export const Cpu = (): BarBoxChild => {
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -48,7 +52,9 @@ export const Cpu = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
labelBinding.drop();
|
||||
cpuService.destroy();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,44 +1,108 @@
|
||||
import { Variable } from 'astal';
|
||||
import { bind, Binding } from 'astal';
|
||||
import CpuTempService from 'src/services/system/cputemp';
|
||||
import { TemperatureConverter } from 'src/lib/units/temperature';
|
||||
import { CpuTempSensorDiscovery } from 'src/services/system/cputemp/sensorDiscovery';
|
||||
import options from 'src/configuration';
|
||||
import GLib from 'gi://GLib?version=2.0';
|
||||
import { convertCelsiusToFahrenheit } from 'src/shared/weather';
|
||||
import options from 'src/options';
|
||||
import { UnitType } from 'src/lib/types/weather.types';
|
||||
const { sensor } = options.bar.customModules.cpuTemp;
|
||||
|
||||
const { pollingInterval, sensor } = options.bar.customModules.cpuTemp;
|
||||
|
||||
/**
|
||||
* Retrieves the current CPU temperature.
|
||||
*
|
||||
* This function reads the CPU temperature from the specified sensor file and converts it to the desired unit (Celsius or Fahrenheit).
|
||||
* It also handles rounding the temperature value based on the provided `round` variable.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the temperature value.
|
||||
* @param unit A Variable<UnitType> indicating the desired unit for the temperature (Celsius or Fahrenheit).
|
||||
*
|
||||
* @returns The current CPU temperature as a number. Returns 0 if an error occurs or the sensor file is empty.
|
||||
* Creates a tooltip for the CPU temperature module showing sensor details
|
||||
*/
|
||||
export const getCPUTemperature = (round: Variable<boolean>, unit: Variable<UnitType>): number => {
|
||||
try {
|
||||
if (sensor.get().length === 0) {
|
||||
return 0;
|
||||
export function getCpuTempTooltip(cpuTempService: CpuTempService): Binding<string> {
|
||||
return bind(cpuTempService.temperature).as((temp) => {
|
||||
const currentPath = cpuTempService.currentSensorPath;
|
||||
const configuredSensor = sensor.get();
|
||||
const isAuto = configuredSensor === 'auto' || configuredSensor === '';
|
||||
|
||||
const tempC = TemperatureConverter.fromCelsius(temp).formatCelsius();
|
||||
const tempF = TemperatureConverter.fromCelsius(temp).formatFahrenheit();
|
||||
|
||||
const lines = [
|
||||
'CPU Temperature',
|
||||
'─────────────────────────',
|
||||
`Current: ${tempC} (${tempF})`,
|
||||
'',
|
||||
'Sensor Information',
|
||||
'─────────────────────────',
|
||||
];
|
||||
|
||||
if (currentPath) {
|
||||
const sensorType = getSensorType(currentPath);
|
||||
const sensorName = getSensorName(currentPath);
|
||||
const chipName = getChipName(currentPath);
|
||||
|
||||
lines.push(`Mode: ${isAuto ? 'Auto-discovered' : 'User-configured'}`, `Type: ${sensorType}`);
|
||||
|
||||
if (chipName) {
|
||||
lines.push(`Chip: ${chipName}`);
|
||||
}
|
||||
|
||||
lines.push(`Device: ${sensorName}`, `Path: ${currentPath}`);
|
||||
} else {
|
||||
lines.push('Status: No sensor found', 'Try setting a manual sensor path');
|
||||
}
|
||||
|
||||
const [success, tempInfoBytes] = GLib.file_get_contents(sensor.get());
|
||||
const tempInfo = new TextDecoder('utf-8').decode(tempInfoBytes);
|
||||
const interval = pollingInterval.get();
|
||||
lines.push('', `Update interval: ${interval}ms`);
|
||||
|
||||
if (!success || tempInfoBytes === null) {
|
||||
console.error(`Failed to read ${sensor.get()} or file content is null.`);
|
||||
return 0;
|
||||
const allSensors = CpuTempSensorDiscovery.getAllSensors();
|
||||
if (allSensors.length > 1) {
|
||||
lines.push('', `Available sensors: ${allSensors.length}`);
|
||||
}
|
||||
|
||||
let decimalTemp = parseInt(tempInfo, 10) / 1000;
|
||||
return lines.join('\n');
|
||||
});
|
||||
}
|
||||
|
||||
if (unit.get() === 'imperial') {
|
||||
decimalTemp = convertCelsiusToFahrenheit(decimalTemp);
|
||||
}
|
||||
/**
|
||||
* Determines sensor type from path
|
||||
*/
|
||||
function getSensorType(path: string): string {
|
||||
if (path.includes('/sys/class/hwmon/')) return 'Hardware Monitor';
|
||||
if (path.includes('/sys/class/thermal/')) return 'Thermal Zone';
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
return round.get() ? Math.round(decimalTemp) : parseFloat(decimalTemp.toFixed(2));
|
||||
} catch (error) {
|
||||
console.error('Error calculating CPU Temp:', error);
|
||||
return 0;
|
||||
/**
|
||||
* Extracts sensor name from path
|
||||
*/
|
||||
function getSensorName(path: string): string {
|
||||
if (path.includes('/sys/class/hwmon/')) {
|
||||
const match = path.match(/hwmon(\d+)/);
|
||||
return match ? `hwmon${match[1]}` : 'Unknown';
|
||||
}
|
||||
};
|
||||
|
||||
if (path.includes('/sys/class/thermal/')) {
|
||||
const match = path.match(/thermal_zone(\d+)/);
|
||||
return match ? `thermal_zone${match[1]}` : 'Unknown';
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the actual chip name for hwmon sensors
|
||||
*/
|
||||
function getChipName(path: string): string | undefined {
|
||||
if (!path.includes('/sys/class/hwmon/')) return undefined;
|
||||
|
||||
try {
|
||||
const match = path.match(/\/sys\/class\/hwmon\/hwmon\d+/);
|
||||
if (!match) return undefined;
|
||||
|
||||
const nameFile = `${match[0]}/name`;
|
||||
const [success, bytes] = GLib.file_get_contents(nameFile);
|
||||
|
||||
if (success && bytes) {
|
||||
return new TextDecoder('utf-8').decode(bytes).trim();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.debug(`Failed to get chip name: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { getCPUTemperature } from './helpers';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { Module } from '../../shared/module';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { UnitType } from 'src/lib/types/weather.types';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import CpuTempService from 'src/services/system/cputemp';
|
||||
import options from 'src/configuration';
|
||||
import { TemperatureConverter } from 'src/lib/units/temperature';
|
||||
import { getCpuTempTooltip } from './helpers';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const {
|
||||
label,
|
||||
@@ -23,37 +25,51 @@ const {
|
||||
icon,
|
||||
} = options.bar.customModules.cpuTemp;
|
||||
|
||||
export const cpuTemp = Variable(0);
|
||||
|
||||
const cpuTempPoller = new FunctionPoller<number, [Variable<boolean>, Variable<UnitType>]>(
|
||||
cpuTemp,
|
||||
[bind(sensor), bind(round), bind(unit)],
|
||||
bind(pollingInterval),
|
||||
getCPUTemperature,
|
||||
round,
|
||||
unit,
|
||||
);
|
||||
|
||||
cpuTempPoller.initialize('cputemp');
|
||||
const cpuTempService = new CpuTempService({ frequency: pollingInterval, sensor });
|
||||
|
||||
export const CpuTemp = (): BarBoxChild => {
|
||||
cpuTempService.initialize();
|
||||
|
||||
const bindings = Variable.derive([bind(sensor), bind(round), bind(unit)], (sensorName) => {
|
||||
cpuTempService.refresh();
|
||||
|
||||
if (cpuTempService.sensor.get() !== sensorName) {
|
||||
cpuTempService.updateSensor(sensorName);
|
||||
}
|
||||
});
|
||||
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(cpuTemp), bind(unit), bind(showUnit), bind(round)],
|
||||
(cpuTmp, tempUnit, shwUnit) => {
|
||||
const unitLabel = tempUnit === 'imperial' ? 'F' : 'C';
|
||||
const unit = shwUnit ? ` ${unitLabel}` : '';
|
||||
return `${cpuTmp.toString()}°${unit}`;
|
||||
[bind(cpuTempService.temperature), bind(unit), bind(showUnit), bind(round)],
|
||||
(cpuTemp, tempUnit, showUnit, roundValue) => {
|
||||
const tempConverter = TemperatureConverter.fromCelsius(cpuTemp);
|
||||
const isImperial = tempUnit === 'imperial';
|
||||
const precision = roundValue ? 0 : 2;
|
||||
|
||||
if (showUnit) {
|
||||
return isImperial
|
||||
? tempConverter.formatFahrenheit(precision)
|
||||
: tempConverter.formatCelsius(precision);
|
||||
}
|
||||
|
||||
const temp = isImperial
|
||||
? tempConverter.toFahrenheit(precision)
|
||||
: tempConverter.toCelsius(precision);
|
||||
|
||||
return temp.toString();
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const cpuTempModule = Module({
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: 'CPU Temperature',
|
||||
tooltipText: getCpuTempTooltip(cpuTempService),
|
||||
boxClass: 'cpu-temp',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -72,7 +88,10 @@ export const CpuTemp = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
cpuTempService.destroy();
|
||||
labelBinding.drop();
|
||||
bindings.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from '../../utils/helpers';
|
||||
import { Module } from '../../shared/module';
|
||||
import Variable from 'astal/variable';
|
||||
import { bind } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { idleInhibit } from 'src/shared/utilities';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { idleInhibit } from 'src/lib/window/visibility';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const { label, onIcon, offIcon, onLabel, offLabel, rightClick, middleClick, scrollUp, scrollDown } =
|
||||
options.bar.customModules.hypridle;
|
||||
@@ -29,6 +31,8 @@ export const Hypridle = (): BarBoxChild => {
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const hypridleModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
tooltipText: bind(idleInhibit).as(
|
||||
@@ -39,7 +43,7 @@ export const Hypridle = (): BarBoxChild => {
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
fn: () => {
|
||||
toggleInhibit();
|
||||
@@ -60,6 +64,7 @@ export const Hypridle = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
iconBinding.drop();
|
||||
labelBinding.drop();
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execAsync, Variable } from 'astal';
|
||||
import options from 'src/options';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const { temperature } = options.bar.customModules.hyprsunset;
|
||||
|
||||
@@ -9,7 +9,7 @@ const { temperature } = options.bar.customModules.hyprsunset;
|
||||
* This command checks if the hyprsunset process is currently running by using the `pgrep` command.
|
||||
* It returns 'yes' if the process is found and 'no' otherwise.
|
||||
*/
|
||||
export const isActiveCommand = "bash -c \"pgrep -x 'hyprsunset' > /dev/null && echo 'yes' || echo 'no'\"";
|
||||
const isActiveCommand = "bash -c \"pgrep -x 'hyprsunset' > /dev/null && echo 'yes' || echo 'no'\"";
|
||||
|
||||
/**
|
||||
* A variable to track the active state of the hyprsunset process.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler, throttleInput } from 'src/components/bar/utils/helpers';
|
||||
import { Module } from '../../shared/module';
|
||||
import { checkSunsetStatus, isActive, toggleSunset } from './helpers';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import options from 'src/configuration';
|
||||
import { throttleInput } from '../../utils/input/throttle';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const {
|
||||
label,
|
||||
@@ -55,6 +58,8 @@ export const Hyprsunset = (): BarBoxChild => {
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const hyprsunsetModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
tooltipText: tooltipBinding(),
|
||||
@@ -63,7 +68,7 @@ export const Hyprsunset = (): BarBoxChild => {
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
fn: () => {
|
||||
throttledToggleSunset();
|
||||
@@ -84,6 +89,7 @@ export const Hyprsunset = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
iconBinding.drop();
|
||||
tooltipBinding.drop();
|
||||
labelBinding.drop();
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import {
|
||||
HyprctlDeviceLayout,
|
||||
HyprctlKeyboard,
|
||||
KbLabelType,
|
||||
} from 'src/lib/types/customModules/kbLayout.types.js';
|
||||
import { LayoutKeys, layoutMap, LayoutValues } from './layouts';
|
||||
import { KbLabelType, HyprctlDeviceLayout, HyprctlKeyboard } from './types';
|
||||
|
||||
/**
|
||||
* Retrieves the keyboard layout from a given JSON string and format.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Create a const object with all layouts
|
||||
const layoutMapObj = {
|
||||
'Abkhazian (Russia)': 'RU (Ab)',
|
||||
Akan: 'GH (Akan)',
|
||||
|
||||
@@ -12,7 +12,7 @@ export type HyprctlKeyboard = {
|
||||
main: boolean;
|
||||
};
|
||||
|
||||
export type HyprctlMouse = {
|
||||
type HyprctlMouse = {
|
||||
address: string;
|
||||
name: string;
|
||||
defaultSpeed: number;
|
||||
@@ -1,12 +1,14 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { Module } from '../../shared/module';
|
||||
import { getKeyboardLayout } from './helpers';
|
||||
import { bind } from 'astal';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { useHook } from 'src/lib/shared/hookHandler';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const { label, labelType, icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } =
|
||||
@@ -22,6 +24,8 @@ function setLabel(self: Astal.Label): void {
|
||||
}
|
||||
|
||||
export const KbInput = (): BarBoxChild => {
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const keyboardModule = Module({
|
||||
textIcon: bind(icon),
|
||||
tooltipText: '',
|
||||
@@ -43,7 +47,7 @@ export const KbInput = (): BarBoxChild => {
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -61,6 +65,9 @@ export const KbInput = (): BarBoxChild => {
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import AstalMpris from 'gi://AstalMpris?version=0.1';
|
||||
import { Variable } from 'astal';
|
||||
import { MediaTags } from 'src/lib/types/audio.types';
|
||||
import { Opt } from 'src/lib/options';
|
||||
import { MediaTags } from './types';
|
||||
|
||||
/**
|
||||
* Retrieves the icon for a given media player.
|
||||
@@ -106,7 +106,10 @@ export const generateMediaLabel = (
|
||||
if (!isValidMediaTag(p1)) {
|
||||
return '';
|
||||
}
|
||||
const value = p1 !== undefined ? mediaTags[p1] : '';
|
||||
let value = p1 !== undefined ? mediaTags[p1] : '';
|
||||
|
||||
value = value?.replace(/\r?\n/g, ' ') ?? '';
|
||||
|
||||
const suffix = p2 !== undefined && p2.length > 0 ? p2.slice(1) : '';
|
||||
return value ? value + suffix : '';
|
||||
},
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import options from 'src/options.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { generateMediaLabel } from './helpers/index.js';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import { onPrimaryClick, onSecondaryClick, onMiddleClick, onScroll } from 'src/lib/shared/eventHandlers';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { activePlayer, mediaAlbum, mediaArtist, mediaTitle } from 'src/shared/media.js';
|
||||
import AstalMpris from 'gi://AstalMpris?version=0.1';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types.js';
|
||||
import { BarBoxChild } from 'src/components/bar/types.js';
|
||||
import { activePlayer, mediaTitle, mediaAlbum, mediaArtist } from 'src/services/media';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
|
||||
const mprisService = AstalMpris.get_default();
|
||||
const {
|
||||
@@ -103,7 +104,7 @@ const Media = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'mediamenu');
|
||||
openDropdownMenu(clicked, event, 'mediamenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { runAsyncCommand, throttledScrollHandler } from '../../utils/helpers.js';
|
||||
import options from '../../../../options.js';
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import { getDistroIcon } from '../../../../lib/utils.js';
|
||||
import { Variable, bind } from 'astal';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import { onPrimaryClick, onSecondaryClick, onMiddleClick, onScroll } from 'src/lib/shared/eventHandlers';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types.js';
|
||||
import { BarBoxChild } from 'src/components/bar/types.js';
|
||||
import { SystemUtilities } from 'src/core/system/SystemUtilities';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
|
||||
const { rightClick, middleClick, scrollUp, scrollDown, autoDetectIcon, icon } = options.bar.launcher;
|
||||
|
||||
const Menu = (): BarBoxChild => {
|
||||
const iconBinding = Variable.derive(
|
||||
[autoDetectIcon, icon],
|
||||
(autoDetect: boolean, iconValue: string): string => (autoDetect ? getDistroIcon() : iconValue),
|
||||
(autoDetect: boolean, iconValue: string): string =>
|
||||
autoDetect ? SystemUtilities.getDistroIcon() : iconValue,
|
||||
);
|
||||
|
||||
const componentClassName = bind(options.theme.bar.buttons.style).as((style: string) => {
|
||||
@@ -60,7 +62,7 @@ const Menu = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'dashboardmenu');
|
||||
openDropdownMenu(clicked, event, 'dashboardmenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { Module } from '../../shared/module';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { inputHandler } from '../../utils/helpers';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
||||
const audioService = wireplumber.audio;
|
||||
@@ -43,6 +45,9 @@ export const Microphone = (): BarBoxChild => {
|
||||
return `${icon} ${description}`;
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const microphoneModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
label: bind(audioService.defaultMicrophone, 'volume').as((vol) => `${Math.round(vol * 100)}%`),
|
||||
@@ -51,7 +56,7 @@ export const Microphone = (): BarBoxChild => {
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -69,6 +74,9 @@ export const Microphone = (): BarBoxChild => {
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
34
src/components/bar/modules/netstat/helpers.ts
Normal file
34
src/components/bar/modules/netstat/helpers.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import NetworkUsageService from 'src/services/system/networkUsage';
|
||||
import { bind, Variable } from 'astal';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const { networkInterface, rateUnit, round, pollingInterval } = options.bar.customModules.netstat;
|
||||
|
||||
export const setupNetworkServiceBindings = (): void => {
|
||||
const networkService = new NetworkUsageService();
|
||||
|
||||
Variable.derive([bind(pollingInterval)], (interval) => {
|
||||
networkService.updateTimer(interval);
|
||||
})();
|
||||
|
||||
Variable.derive([bind(networkInterface)], (interfaceName) => {
|
||||
networkService.setInterface(interfaceName);
|
||||
})();
|
||||
|
||||
Variable.derive([bind(rateUnit)], (unit) => {
|
||||
networkService.setRateUnit(unit);
|
||||
})();
|
||||
|
||||
Variable.derive([bind(round)], (shouldRound) => {
|
||||
networkService.setShouldRound(shouldRound);
|
||||
})();
|
||||
};
|
||||
|
||||
export const cycleArray = <T>(array: T[], current: T, direction: 'next' | 'prev'): T => {
|
||||
const currentIndex = array.indexOf(current);
|
||||
const nextIndex =
|
||||
direction === 'next'
|
||||
? (currentIndex + 1) % array.length
|
||||
: (currentIndex - 1 + array.length) % array.length;
|
||||
return array[nextIndex];
|
||||
};
|
||||
@@ -1,168 +0,0 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import { Variable } from 'astal';
|
||||
import { RateUnit } from 'src/lib/types/bar.types';
|
||||
import { NetworkResourceData } from 'src/lib/types/customModules/network.types';
|
||||
import { getDefaultNetstatData } from 'src/lib/types/defaults/netstat.types';
|
||||
|
||||
let previousNetUsage = { rx: 0, tx: 0, time: 0 };
|
||||
|
||||
interface NetworkUsage {
|
||||
name: string;
|
||||
rx: number;
|
||||
tx: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the network rate based on the provided rate, type, and rounding option.
|
||||
*
|
||||
* This function converts the network rate to the appropriate unit (KiB/s, MiB/s, GiB/s, or bytes/s) based on the provided type.
|
||||
* It also rounds the rate to the specified number of decimal places.
|
||||
*
|
||||
* @param rate The network rate to format.
|
||||
* @param type The unit type for the rate (KiB, MiB, GiB).
|
||||
* @param round A boolean indicating whether to round the rate.
|
||||
*
|
||||
* @returns The formatted network rate as a string.
|
||||
*/
|
||||
const formatRate = (rate: number, type: string, round: boolean): string => {
|
||||
const fixed = round ? 0 : 2;
|
||||
|
||||
switch (true) {
|
||||
case type === 'KiB':
|
||||
return `${(rate / 1e3).toFixed(fixed)} KiB/s`;
|
||||
case type === 'MiB':
|
||||
return `${(rate / 1e6).toFixed(fixed)} MiB/s`;
|
||||
case type === 'GiB':
|
||||
return `${(rate / 1e9).toFixed(fixed)} GiB/s`;
|
||||
case rate >= 1e9:
|
||||
return `${(rate / 1e9).toFixed(fixed)} GiB/s`;
|
||||
case rate >= 1e6:
|
||||
return `${(rate / 1e6).toFixed(fixed)} MiB/s`;
|
||||
case rate >= 1e3:
|
||||
return `${(rate / 1e3).toFixed(fixed)} KiB/s`;
|
||||
default:
|
||||
return `${rate.toFixed(fixed)} bytes/s`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a line of network interface data.
|
||||
*
|
||||
* This function parses a line of network interface data from the /proc/net/dev file.
|
||||
* It extracts the interface name, received bytes, and transmitted bytes.
|
||||
*
|
||||
* @param line The line of network interface data to parse.
|
||||
*
|
||||
* @returns An object containing the interface name, received bytes, and transmitted bytes, or null if the line is invalid.
|
||||
*/
|
||||
const parseInterfaceData = (line: string): NetworkUsage | null => {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine || trimmedLine.startsWith('Inter-') || trimmedLine.startsWith('face')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [iface, rx, , , , , , , , tx] = trimmedLine.split(/\s+/);
|
||||
const rxValue = parseInt(rx, 10);
|
||||
const txValue = parseInt(tx, 10);
|
||||
const cleanedIface = iface.replace(':', '');
|
||||
|
||||
return { name: cleanedIface, rx: rxValue, tx: txValue };
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a network interface.
|
||||
*
|
||||
* This function checks if the provided network interface is valid based on the interface name and received/transmitted bytes.
|
||||
*
|
||||
* @param iface The network interface to validate.
|
||||
* @param interfaceName The name of the interface to check.
|
||||
*
|
||||
* @returns True if the interface is valid, false otherwise.
|
||||
*/
|
||||
const isValidInterface = (iface: NetworkUsage | null, interfaceName: string): boolean => {
|
||||
if (!iface) return false;
|
||||
if (interfaceName) return iface.name === interfaceName;
|
||||
return iface.name !== 'lo' && iface.rx > 0 && iface.tx > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the network usage for a specified interface.
|
||||
*
|
||||
* This function reads the /proc/net/dev file to get the network usage data for the specified interface.
|
||||
* If no interface name is provided, it returns the usage data for the first valid interface found.
|
||||
*
|
||||
* @param interfaceName The name of the interface to get the usage data for. Defaults to an empty string.
|
||||
*
|
||||
* @returns An object containing the interface name, received bytes, and transmitted bytes.
|
||||
*/
|
||||
const getNetworkUsage = (interfaceName: string = ''): NetworkUsage => {
|
||||
const [success, data] = GLib.file_get_contents('/proc/net/dev');
|
||||
const defaultStats = { name: '', rx: 0, tx: 0 };
|
||||
|
||||
if (!success) {
|
||||
console.error('Failed to read /proc/net/dev');
|
||||
return defaultStats;
|
||||
}
|
||||
|
||||
const lines = new TextDecoder('utf-8').decode(data).split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const iface = parseInterfaceData(line);
|
||||
|
||||
if (isValidInterface(iface, interfaceName)) {
|
||||
return iface ?? defaultStats;
|
||||
}
|
||||
}
|
||||
|
||||
return { name: '', rx: 0, tx: 0 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the network usage data.
|
||||
*
|
||||
* This function calculates the network usage data based on the provided rounding option, interface name, and data type.
|
||||
* It returns an object containing the formatted received and transmitted rates.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the rates.
|
||||
* @param interfaceNameVar A Variable<string> containing the name of the interface to get the usage data for.
|
||||
* @param dataType A Variable<RateUnit> containing the unit type for the rates.
|
||||
*
|
||||
* @returns An object containing the formatted received and transmitted rates.
|
||||
*/
|
||||
export const computeNetwork = (
|
||||
round: Variable<boolean>,
|
||||
interfaceNameVar: Variable<string>,
|
||||
dataType: Variable<RateUnit>,
|
||||
): NetworkResourceData => {
|
||||
const rateUnit = dataType.get();
|
||||
const interfaceName = interfaceNameVar.get();
|
||||
|
||||
const DEFAULT_NETSTAT_DATA = getDefaultNetstatData(rateUnit);
|
||||
try {
|
||||
const { rx, tx, name } = getNetworkUsage(interfaceName);
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!name) {
|
||||
return DEFAULT_NETSTAT_DATA;
|
||||
}
|
||||
|
||||
if (previousNetUsage.time === 0) {
|
||||
previousNetUsage = { rx, tx, time: currentTime };
|
||||
return DEFAULT_NETSTAT_DATA;
|
||||
}
|
||||
|
||||
const timeDiff = Math.max((currentTime - previousNetUsage.time) / 1000, 1);
|
||||
const rxRate = (rx - previousNetUsage.rx) / timeDiff;
|
||||
const txRate = (tx - previousNetUsage.tx) / timeDiff;
|
||||
|
||||
previousNetUsage = { rx, tx, time: currentTime };
|
||||
|
||||
return {
|
||||
in: formatRate(rxRate, rateUnit, round.get()),
|
||||
out: formatRate(txRate, rateUnit, round.get()),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating network usage:', error);
|
||||
return DEFAULT_NETSTAT_DATA;
|
||||
}
|
||||
};
|
||||
@@ -1,91 +1,87 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { computeNetwork } from './helpers';
|
||||
import { NETWORK_LABEL_TYPES } from 'src/lib/types/defaults/bar.types';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { Module } from '../../shared/module';
|
||||
import NetworkUsageService from 'src/services/system/networkUsage';
|
||||
import { bind, Variable } from 'astal';
|
||||
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { RateUnit, BarBoxChild, NetstatLabelType } from 'src/lib/types/bar.types';
|
||||
import { NetworkResourceData } from 'src/lib/types/customModules/network.types';
|
||||
import { getDefaultNetstatData } from 'src/lib/types/defaults/netstat.types';
|
||||
import { BarBoxChild } from '../../types';
|
||||
import { NetstatLabelType } from 'src/services/system/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import options from 'src/configuration';
|
||||
import { cycleArray, setupNetworkServiceBindings } from './helpers';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
const astalNetworkService = AstalNetwork.get_default();
|
||||
|
||||
const NETWORK_LABEL_TYPES: NetstatLabelType[] = ['full', 'in', 'out'];
|
||||
|
||||
const networkService = AstalNetwork.get_default();
|
||||
const {
|
||||
label,
|
||||
labelType,
|
||||
networkInterface,
|
||||
rateUnit,
|
||||
dynamicIcon,
|
||||
icon,
|
||||
networkInLabel,
|
||||
networkOutLabel,
|
||||
round,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
pollingInterval,
|
||||
} = options.bar.customModules.netstat;
|
||||
|
||||
export const networkUsage = Variable<NetworkResourceData>(getDefaultNetstatData(rateUnit.get()));
|
||||
setupNetworkServiceBindings();
|
||||
|
||||
const netstatPoller = new FunctionPoller<
|
||||
NetworkResourceData,
|
||||
[round: Variable<boolean>, interfaceNameVar: Variable<string>, dataType: Variable<RateUnit>]
|
||||
>(
|
||||
networkUsage,
|
||||
[bind(rateUnit), bind(networkInterface), bind(round)],
|
||||
bind(pollingInterval),
|
||||
computeNetwork,
|
||||
round,
|
||||
networkInterface,
|
||||
rateUnit,
|
||||
);
|
||||
|
||||
netstatPoller.initialize('netstat');
|
||||
const networkService = new NetworkUsageService({ frequency: pollingInterval });
|
||||
|
||||
export const Netstat = (): BarBoxChild => {
|
||||
const renderNetworkLabel = (lblType: NetstatLabelType, networkService: NetworkResourceData): string => {
|
||||
networkService.initialize();
|
||||
|
||||
const renderNetworkLabel = (
|
||||
lblType: NetstatLabelType,
|
||||
networkData: { in: string; out: string },
|
||||
): string => {
|
||||
switch (lblType) {
|
||||
case 'in':
|
||||
return `${networkInLabel.get()} ${networkService.in}`;
|
||||
return `${networkInLabel.get()} ${networkData.in}`;
|
||||
case 'out':
|
||||
return `${networkOutLabel.get()} ${networkService.out}`;
|
||||
return `${networkOutLabel.get()} ${networkData.out}`;
|
||||
default:
|
||||
return `${networkInLabel.get()} ${networkService.in} ${networkOutLabel.get()} ${networkService.out}`;
|
||||
return `${networkInLabel.get()} ${networkData.in} ${networkOutLabel.get()} ${networkData.out}`;
|
||||
}
|
||||
};
|
||||
|
||||
const iconBinding = Variable.derive(
|
||||
[bind(networkService, 'primary'), bind(networkService, 'wifi'), bind(networkService, 'wired')],
|
||||
(pmry, wfi, wrd) => {
|
||||
if (pmry === AstalNetwork.Primary.WIRED) {
|
||||
return wrd?.icon_name;
|
||||
[
|
||||
bind(astalNetworkService, 'primary'),
|
||||
bind(astalNetworkService, 'wifi'),
|
||||
bind(astalNetworkService, 'wired'),
|
||||
],
|
||||
(primary, wifi, wired) => {
|
||||
if (primary === AstalNetwork.Primary.WIRED) {
|
||||
return wired?.icon_name;
|
||||
}
|
||||
return wfi?.icon_name;
|
||||
return wifi?.icon_name;
|
||||
},
|
||||
);
|
||||
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(networkUsage), bind(labelType)],
|
||||
(networkService: NetworkResourceData, lblTyp: NetstatLabelType) =>
|
||||
renderNetworkLabel(lblTyp, networkService),
|
||||
[bind(networkService.network), bind(labelType)],
|
||||
(networkData, lblType: NetstatLabelType) => renderNetworkLabel(lblType, networkData),
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const netstatModule = Module({
|
||||
useTextIcon: bind(dynamicIcon).as((useDynamicIcon) => !useDynamicIcon),
|
||||
icon: iconBinding(),
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: bind(labelType).as((lblTyp) => {
|
||||
return lblTyp === 'full' ? 'Ingress / Egress' : lblTyp === 'in' ? 'Ingress' : 'Egress';
|
||||
tooltipText: bind(labelType).as((lblType) => {
|
||||
return lblType === 'full' ? 'Ingress / Egress' : lblType === 'in' ? 'Ingress' : 'Egress';
|
||||
}),
|
||||
boxClass: 'netstat',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -97,31 +93,23 @@ export const Netstat = (): BarBoxChild => {
|
||||
},
|
||||
onScrollUp: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
NETWORK_LABEL_TYPES[
|
||||
(NETWORK_LABEL_TYPES.indexOf(labelType.get()) + 1) %
|
||||
NETWORK_LABEL_TYPES.length
|
||||
] as NetstatLabelType,
|
||||
);
|
||||
const nextLabelType = cycleArray(NETWORK_LABEL_TYPES, labelType.get(), 'next');
|
||||
labelType.set(nextLabelType);
|
||||
},
|
||||
},
|
||||
onScrollDown: {
|
||||
fn: () => {
|
||||
labelType.set(
|
||||
NETWORK_LABEL_TYPES[
|
||||
(NETWORK_LABEL_TYPES.indexOf(labelType.get()) -
|
||||
1 +
|
||||
NETWORK_LABEL_TYPES.length) %
|
||||
NETWORK_LABEL_TYPES.length
|
||||
] as NetstatLabelType,
|
||||
);
|
||||
const prevLabelType = cycleArray(NETWORK_LABEL_TYPES, labelType.get(), 'prev');
|
||||
labelType.set(prevLabelType);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
labelBinding.drop();
|
||||
iconBinding.drop();
|
||||
networkService.destroy();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import options from 'src/options';
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { onPrimaryClick, onSecondaryClick, onMiddleClick, onScroll } from 'src/lib/shared/eventHandlers';
|
||||
import { Astal, Gtk } from 'astal/gtk3';
|
||||
import AstalNetwork from 'gi://AstalNetwork?version=0.1';
|
||||
import { formatWifiInfo, wiredIcon, wirelessIcon } from './helpers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
|
||||
const networkService = AstalNetwork.get_default();
|
||||
const { label, truncation, truncation_size, rightClick, middleClick, scrollDown, scrollUp, showWifiInfo } =
|
||||
@@ -46,10 +47,7 @@ const Network = (): BarBoxChild => {
|
||||
);
|
||||
}
|
||||
const networkWifi = networkService.wifi;
|
||||
if (networkWifi != null) {
|
||||
// Astal doesn't reset the wifi attributes on disconnect, only on a valid connection
|
||||
// so we need to check if both the WiFi is enabled and if there is an active access
|
||||
// point
|
||||
if (networkWifi !== null) {
|
||||
if (!networkWifi.enabled) {
|
||||
return <label className={'bar-button-label network-label'} label="Off" />;
|
||||
}
|
||||
@@ -127,7 +125,7 @@ const Network = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'networkmenu');
|
||||
openDropdownMenu(clicked, event, 'networkmenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import AstalNotifd from 'gi://AstalNotifd?version=0.1';
|
||||
import { Astal, Gtk } from 'astal/gtk3';
|
||||
import { openMenu } from '../../utils/menu';
|
||||
import options from 'src/options';
|
||||
import { filterNotifications } from 'src/lib/shared/notifications.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
import { filterNotifications } from 'src/lib/shared/notifications';
|
||||
|
||||
const notifdService = AstalNotifd.get_default();
|
||||
const { show_total, rightClick, middleClick, scrollUp, scrollDown, hideCountWhenZero } =
|
||||
@@ -107,7 +108,7 @@ export const Notifications = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'notificationsmenu');
|
||||
openDropdownMenu(clicked, event, 'notificationsmenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { Module } from '../../shared/module';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const { icon, leftClick, rightClick, middleClick, scrollUp, scrollDown } = options.bar.customModules.power;
|
||||
|
||||
export const Power = (): BarBoxChild => {
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const powerModule = Module({
|
||||
tooltipText: 'Power Menu',
|
||||
textIcon: bind(icon),
|
||||
@@ -15,7 +19,7 @@ export const Power = (): BarBoxChild => {
|
||||
boxClass: 'powermodule',
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -33,6 +37,9 @@ export const Power = (): BarBoxChild => {
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { divide } from 'src/components/bar/utils/helpers';
|
||||
import { GLib, Variable } from 'astal';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic.types';
|
||||
|
||||
/**
|
||||
* Calculates the RAM usage.
|
||||
*
|
||||
* This function reads the memory information from the /proc/meminfo file and calculates the total, used, and available RAM.
|
||||
* It returns an object containing these values along with the percentage of used RAM.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the percentage value.
|
||||
*
|
||||
* @returns An object containing the total, used, free RAM in bytes, and the percentage of used RAM.
|
||||
*/
|
||||
export const calculateRamUsage = (round: Variable<boolean>): GenericResourceData => {
|
||||
try {
|
||||
const [success, meminfoBytes] = GLib.file_get_contents('/proc/meminfo');
|
||||
|
||||
if (!success || meminfoBytes === null) {
|
||||
throw new Error('Failed to read /proc/meminfo or file content is null.');
|
||||
}
|
||||
|
||||
const meminfo = new TextDecoder('utf-8').decode(meminfoBytes);
|
||||
|
||||
const totalMatch = meminfo.match(/MemTotal:\s+(\d+)/);
|
||||
const availableMatch = meminfo.match(/MemAvailable:\s+(\d+)/);
|
||||
|
||||
if (!totalMatch || !availableMatch) {
|
||||
throw new Error('Failed to parse /proc/meminfo for memory values.');
|
||||
}
|
||||
|
||||
const totalRamInBytes = parseInt(totalMatch[1], 10) * 1024;
|
||||
const availableRamInBytes = parseInt(availableMatch[1], 10) * 1024;
|
||||
|
||||
let usedRam = totalRamInBytes - availableRamInBytes;
|
||||
usedRam = isNaN(usedRam) || usedRam < 0 ? 0 : usedRam;
|
||||
|
||||
return {
|
||||
percentage: divide([totalRamInBytes, usedRam], round.get()),
|
||||
total: totalRamInBytes,
|
||||
used: usedRam,
|
||||
free: availableRamInBytes,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating RAM usage:', error);
|
||||
return { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
}
|
||||
};
|
||||
@@ -1,33 +1,25 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { calculateRamUsage } from './helpers';
|
||||
import { formatTooltip, inputHandler, renderResourceLabel } from 'src/components/bar/utils/helpers';
|
||||
import { LABEL_TYPES } from 'src/lib/types/defaults/bar.types';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { Module } from '../../shared/module';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild, ResourceLabelType } from 'src/lib/types/bar.types';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { renderResourceLabel, formatTooltip } from '../../utils/systemResource';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import { GenericResourceData, ResourceLabelType, LABEL_TYPES } from 'src/services/system/types';
|
||||
import RamUsageService from 'src/services/system/ramUsage';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const { label, labelType, round, leftClick, rightClick, middleClick, pollingInterval, icon } =
|
||||
options.bar.customModules.ram;
|
||||
|
||||
const defaultRamData: GenericResourceData = { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
const ramUsage = Variable<GenericResourceData>(defaultRamData);
|
||||
|
||||
const ramPoller = new FunctionPoller<GenericResourceData, [Variable<boolean>]>(
|
||||
ramUsage,
|
||||
[bind(round)],
|
||||
bind(pollingInterval),
|
||||
calculateRamUsage,
|
||||
round,
|
||||
);
|
||||
|
||||
ramPoller.initialize('ram');
|
||||
const ramService = new RamUsageService({ frequency: pollingInterval });
|
||||
|
||||
export const Ram = (): BarBoxChild => {
|
||||
ramService.initialize();
|
||||
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(ramUsage), bind(labelType), bind(round)],
|
||||
[bind(ramService.ram), bind(labelType), bind(round)],
|
||||
(rmUsg: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
|
||||
const returnValue = renderResourceLabel(lblTyp, rmUsg, round);
|
||||
|
||||
@@ -35,6 +27,8 @@ export const Ram = (): BarBoxChild => {
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const ramModule = Module({
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
@@ -45,7 +39,7 @@ export const Ram = (): BarBoxChild => {
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -77,7 +71,9 @@ export const Ram = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
labelBinding.drop();
|
||||
ramService.destroy();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import GTop from 'gi://GTop';
|
||||
|
||||
import { divide } from 'src/components/bar/utils/helpers';
|
||||
import { Variable } from 'astal';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic.types';
|
||||
|
||||
/**
|
||||
* Computes the storage usage for the root filesystem.
|
||||
*
|
||||
* This function calculates the total, used, and available storage for the root filesystem.
|
||||
* It returns an object containing these values along with the percentage of used storage.
|
||||
*
|
||||
* @param round A Variable<boolean> indicating whether to round the percentage value.
|
||||
*
|
||||
* @returns An object containing the total, used, free storage in bytes, and the percentage of used storage.
|
||||
*
|
||||
* FIX: Consolidate with Storage service class
|
||||
*/
|
||||
export const computeStorage = (round: Variable<boolean>): GenericResourceData => {
|
||||
try {
|
||||
const currentFsUsage = new GTop.glibtop_fsusage();
|
||||
|
||||
GTop.glibtop_get_fsusage(currentFsUsage, '/');
|
||||
|
||||
const total = currentFsUsage.blocks * currentFsUsage.block_size;
|
||||
const available = currentFsUsage.bavail * currentFsUsage.block_size;
|
||||
const used = total - available;
|
||||
|
||||
return {
|
||||
total,
|
||||
used,
|
||||
free: available,
|
||||
percentage: divide([total, used], round.get()),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating RAM usage:', error);
|
||||
return { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
import { DriveStorageData } from 'src/services/system/storage/types';
|
||||
import StorageService from 'src/services/system/storage';
|
||||
import { renderResourceLabel } from 'src/components/bar/utils/systemResource';
|
||||
import { SizeUnit } from 'src/lib/units/size/types';
|
||||
|
||||
export type TooltipStyle = 'percentage-bar' | 'tree' | 'simple';
|
||||
|
||||
/**
|
||||
* Formats storage tooltip information based on the selected style
|
||||
* @param paths - Array of mount paths to display
|
||||
* @param storageService - The storage service instance
|
||||
* @param style - The tooltip formatting style
|
||||
* @param lblTyp - The label type for resource display
|
||||
* @param round - Whether to round values
|
||||
* @param sizeUnits - The size unit to use
|
||||
*/
|
||||
export function formatStorageTooltip(
|
||||
paths: string[],
|
||||
storageService: StorageService,
|
||||
style: TooltipStyle,
|
||||
round: boolean,
|
||||
sizeUnits?: SizeUnit,
|
||||
): string {
|
||||
const driveData = paths
|
||||
.map((path) => storageService.getDriveInfo(path))
|
||||
.filter((usage): usage is DriveStorageData => usage !== undefined);
|
||||
|
||||
switch (style) {
|
||||
case 'percentage-bar':
|
||||
return formatPercentageBarStyle(driveData, round, sizeUnits);
|
||||
case 'tree':
|
||||
return formatTreeStyle(driveData, round, sizeUnits);
|
||||
case 'simple':
|
||||
default:
|
||||
return formatSimpleStyle(driveData, round, sizeUnits);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a visual percentage bar using Unicode characters
|
||||
* @param percentage - The percentage value (0-100)
|
||||
*/
|
||||
function generatePercentBar(percentage: number): string {
|
||||
const filledBlocks = Math.round(percentage / 10);
|
||||
const emptyBlocks = 10 - filledBlocks;
|
||||
return '▰'.repeat(filledBlocks) + '▱'.repeat(emptyBlocks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats tooltip with visual percentage bars
|
||||
*/
|
||||
function formatPercentageBarStyle(drives: DriveStorageData[], round: boolean, sizeUnits?: SizeUnit): string {
|
||||
return drives
|
||||
.map((usage) => {
|
||||
const lbl = renderResourceLabel('used/total', usage, round, sizeUnits);
|
||||
const percentBar = generatePercentBar(usage.percentage);
|
||||
const displayName = usage.path === '/' ? '◉ System' : `◉ ${usage.name}`;
|
||||
|
||||
return `${displayName}\n ${percentBar} ${usage.percentage.toFixed(1)}%\n ${lbl}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats tooltip with tree-like structure
|
||||
*/
|
||||
function formatTreeStyle(drives: DriveStorageData[], round: boolean, sizeUnits?: SizeUnit): string {
|
||||
return drives
|
||||
.map((usage) => {
|
||||
const lbl = renderResourceLabel('used/total', usage, round, sizeUnits);
|
||||
const displayName = usage.path === '/' ? 'System' : usage.name;
|
||||
|
||||
return `• ${displayName}: ${usage.percentage.toFixed(1)}%\n └─ ${lbl}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats tooltip with simple text layout
|
||||
*/
|
||||
function formatSimpleStyle(drives: DriveStorageData[], round: boolean, sizeUnits?: SizeUnit): string {
|
||||
return drives
|
||||
.map((usage) => {
|
||||
const lbl = renderResourceLabel('used/total', usage, round, sizeUnits);
|
||||
const displayName = usage.path === '/' ? 'System' : usage.name;
|
||||
|
||||
return `[${displayName}]: ${lbl}`;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
@@ -1,49 +1,68 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { formatTooltip, inputHandler, renderResourceLabel } from 'src/components/bar/utils/helpers';
|
||||
import { computeStorage } from './helpers';
|
||||
import { LABEL_TYPES } from 'src/lib/types/defaults/bar.types';
|
||||
import { FunctionPoller } from 'src/lib/poller/FunctionPoller';
|
||||
import { Module } from '../../shared/module';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild, ResourceLabelType } from 'src/lib/types/bar.types';
|
||||
import { GenericResourceData } from 'src/lib/types/customModules/generic.types';
|
||||
import options from 'src/configuration';
|
||||
import { renderResourceLabel } from '../../utils/systemResource';
|
||||
import { LABEL_TYPES, ResourceLabelType } from 'src/services/system/types';
|
||||
import { BarBoxChild } from '../../types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import StorageService from 'src/services/system/storage';
|
||||
import { formatStorageTooltip } from './helpers/tooltipFormatters';
|
||||
|
||||
const { label, labelType, icon, round, leftClick, rightClick, middleClick, pollingInterval } =
|
||||
options.bar.customModules.storage;
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const defaultStorageData = { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
|
||||
const storageUsage = Variable<GenericResourceData>(defaultStorageData);
|
||||
|
||||
const storagePoller = new FunctionPoller<GenericResourceData, [Variable<boolean>]>(
|
||||
storageUsage,
|
||||
[bind(round)],
|
||||
bind(pollingInterval),
|
||||
computeStorage,
|
||||
const {
|
||||
label,
|
||||
labelType,
|
||||
icon,
|
||||
round,
|
||||
);
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
pollingInterval,
|
||||
units,
|
||||
tooltipStyle,
|
||||
paths,
|
||||
} = options.bar.customModules.storage;
|
||||
|
||||
storagePoller.initialize('storage');
|
||||
const storageService = new StorageService({ frequency: pollingInterval, round, pathsToMonitor: paths });
|
||||
|
||||
export const Storage = (): BarBoxChild => {
|
||||
const tooltipText = Variable('');
|
||||
|
||||
storageService.initialize();
|
||||
|
||||
const labelBinding = Variable.derive(
|
||||
[bind(storageUsage), bind(labelType), bind(round)],
|
||||
(storage, lblTyp, round) => {
|
||||
return renderResourceLabel(lblTyp, storage, round);
|
||||
[bind(storageService.storage), bind(labelType), bind(paths), bind(tooltipStyle)],
|
||||
(storage, lblTyp, filePaths) => {
|
||||
const storageUnitToUse = units.get();
|
||||
const sizeUnits = storageUnitToUse !== 'auto' ? storageUnitToUse : undefined;
|
||||
|
||||
const tooltipFormatted = formatStorageTooltip(
|
||||
filePaths,
|
||||
storageService,
|
||||
tooltipStyle.get(),
|
||||
round.get(),
|
||||
sizeUnits,
|
||||
);
|
||||
|
||||
tooltipText.set(tooltipFormatted);
|
||||
|
||||
return renderResourceLabel(lblTyp, storage, round.get(), sizeUnits);
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const storageModule = Module({
|
||||
textIcon: bind(icon),
|
||||
label: labelBinding(),
|
||||
tooltipText: bind(labelType).as((lblTyp) => {
|
||||
return formatTooltip('Storage', lblTyp);
|
||||
}),
|
||||
tooltipText: bind(tooltipText),
|
||||
boxClass: 'storage',
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -75,6 +94,7 @@ export const Storage = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
labelBinding.drop();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { capitalizeFirstLetter } from 'src/lib/utils';
|
||||
import { Module } from '../../shared/module';
|
||||
import { getInitialSubmap, isSubmapEnabled } from './helpers';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import options from 'src/configuration';
|
||||
import { capitalizeFirstLetter } from 'src/lib/string/formatters';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const {
|
||||
@@ -52,6 +54,8 @@ export const Submap = (): BarBoxChild => {
|
||||
},
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const submapModule = Module({
|
||||
textIcon: submapIcon(),
|
||||
tooltipText: submapLabel(),
|
||||
@@ -60,7 +64,7 @@ export const Submap = (): BarBoxChild => {
|
||||
boxClass: 'submap',
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -79,6 +83,7 @@ export const Submap = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
submapLabel.drop();
|
||||
submapIcon.drop();
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { isMiddleClick, isPrimaryClick, isSecondaryClick, Notify } from '../../../../lib/utils';
|
||||
import options from '../../../../options';
|
||||
import AstalTray from 'gi://AstalTray?version=0.1';
|
||||
import { bind, Gio, Variable } from 'astal';
|
||||
import { Gdk, Gtk } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { isPrimaryClick, isSecondaryClick, isMiddleClick } from 'src/lib/events/mouse';
|
||||
import { SystemUtilities } from 'src/core/system/SystemUtilities';
|
||||
|
||||
const systemtray = AstalTray.get_default();
|
||||
const { ignore, customIcons } = options.bar.systray;
|
||||
|
||||
//TODO: Connect to `notify::menu-model` and `notify::action-group` to have up to date menu and action group
|
||||
const createMenu = (menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.Menu => {
|
||||
const menu = Gtk.Menu.new_from_model(menuModel);
|
||||
menu.insert_action_group('dbusmenu', actionGroup);
|
||||
@@ -31,7 +31,7 @@ const MenuDefaultIcon = ({ item }: MenuEntryProps): JSX.Element => {
|
||||
return (
|
||||
<icon
|
||||
className={'systray-icon'}
|
||||
gIcon={bind(item, 'gicon')}
|
||||
gicon={bind(item, 'gicon')}
|
||||
tooltipMarkup={bind(item, 'tooltipMarkup')}
|
||||
/>
|
||||
);
|
||||
@@ -67,7 +67,7 @@ const MenuEntry = ({ item, child }: MenuEntryProps): JSX.Element => {
|
||||
}
|
||||
|
||||
if (isMiddleClick(event)) {
|
||||
Notify({ summary: 'App Name', body: item.id });
|
||||
SystemUtilities.notify({ summary: 'App Name', body: item.id });
|
||||
}
|
||||
}}
|
||||
onDestroy={() => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { Module } from '../../shared/module';
|
||||
import { BashPoller } from 'src/lib/poller/BashPoller';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const {
|
||||
updateCommand,
|
||||
@@ -71,6 +73,8 @@ const updatesIcon = Variable.derive(
|
||||
);
|
||||
|
||||
export const Updates = (): BarBoxChild => {
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const updatesModule = Module({
|
||||
textIcon: updatesIcon(),
|
||||
tooltipText: bind(pendingUpdatesTooltip),
|
||||
@@ -80,7 +84,7 @@ export const Updates = (): BarBoxChild => {
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(
|
||||
inputHandlerBindings = inputHandler.attachHandlers(
|
||||
self,
|
||||
{
|
||||
onPrimaryClick: {
|
||||
@@ -102,6 +106,9 @@ export const Updates = (): BarBoxChild => {
|
||||
postInputUpdater,
|
||||
);
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { openMenu } from '../../utils/menu.js';
|
||||
import options from 'src/options';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers.js';
|
||||
import { onPrimaryClick, onSecondaryClick, onMiddleClick, onScroll } from 'src/lib/shared/eventHandlers';
|
||||
import { getIcon } from './helpers/index.js';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types.js';
|
||||
import { BarBoxChild } from 'src/components/bar/types.js';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
import { openDropdownMenu } from '../../utils/menu';
|
||||
|
||||
const wireplumber = AstalWp.get_default() as AstalWp.Wp;
|
||||
const audioService = wireplumber?.audio;
|
||||
@@ -102,7 +103,7 @@ const Volume = (): BarBoxChild => {
|
||||
|
||||
disconnectFunctions.push(
|
||||
onPrimaryClick(self, (clicked, event) => {
|
||||
openMenu(clicked, event, 'audiomenu');
|
||||
openDropdownMenu(clicked, event, 'audiomenu');
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
import options from 'src/options';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers';
|
||||
import { getWeatherStatusTextIcon, globalWeatherVar } from 'src/shared/weather';
|
||||
import { Module } from '../../shared/module';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import WeatherService from 'src/services/weather';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
import options from 'src/configuration';
|
||||
import { toTitleCase } from 'src/lib/string/formatters';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const weatherService = WeatherService.getInstance();
|
||||
|
||||
const { label, unit, leftClick, rightClick, middleClick, scrollUp, scrollDown } =
|
||||
options.bar.customModules.weather;
|
||||
|
||||
export const Weather = (): BarBoxChild => {
|
||||
const iconBinding = Variable.derive([bind(globalWeatherVar)], (wthr) => {
|
||||
const weatherStatusIcon = getWeatherStatusTextIcon(wthr);
|
||||
return weatherStatusIcon;
|
||||
const iconBinding = Variable.derive([bind(weatherService.statusIcon)], (icon) => {
|
||||
return icon;
|
||||
});
|
||||
|
||||
const labelBinding = Variable.derive([bind(globalWeatherVar), bind(unit)], (wthr, unt) => {
|
||||
if (unt === 'imperial') {
|
||||
return `${Math.ceil(wthr.current.temp_f)}° F`;
|
||||
} else {
|
||||
return `${Math.ceil(wthr.current.temp_c)}° C`;
|
||||
}
|
||||
const labelBinding = Variable.derive([bind(weatherService.temperature), bind(unit)], (temp) => {
|
||||
return temp;
|
||||
});
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const weatherModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
tooltipText: bind(globalWeatherVar).as((v) => `Weather Status: ${v.current.condition.text}`),
|
||||
tooltipText: bind(weatherService.weatherData).as(
|
||||
(wthr) => `Weather Status: ${toTitleCase(wthr.current.condition.text)}`,
|
||||
),
|
||||
boxClass: 'weather-custom',
|
||||
label: labelBinding(),
|
||||
showLabelBinding: bind(label),
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -50,6 +54,7 @@ export const Weather = (): BarBoxChild => {
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
iconBinding.drop();
|
||||
labelBinding.drop();
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import options from 'src/options';
|
||||
import { capitalizeFirstLetter } from 'src/lib/utils';
|
||||
import { defaultWindowTitleMap } from 'src/lib/constants/appIcons';
|
||||
import { defaultWindowTitleMap } from 'src/components/bar/modules/window_title/helpers/appIcons';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { bind, Variable } from 'astal';
|
||||
import options from 'src/configuration';
|
||||
import { capitalizeFirstLetter } from 'src/lib/string/formatters';
|
||||
|
||||
const { title_map: userDefinedTitles } = options.bar.windowtitle;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers';
|
||||
import options from 'src/options';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { clientTitle, getTitle, getWindowMatch, truncateTitle } from './helpers/title';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { runAsyncCommand } from '../../utils/input/commandExecutor';
|
||||
import { throttledScrollHandler } from '../../utils/input/throttle';
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const { leftClick, rightClick, middleClick, scrollDown, scrollUp } = options.bar.windowtitle;
|
||||
|
||||
@@ -1,406 +1,278 @@
|
||||
import { Variable } from 'astal';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { MonitorMap, WorkspaceMonitorMap, WorkspaceRule } from 'src/lib/types/workspace.types';
|
||||
import { range } from 'src/lib/utils';
|
||||
import options from 'src/options';
|
||||
import options from 'src/configuration';
|
||||
import { defaultApplicationIconMap } from 'src/components/bar/modules/window_title/helpers/appIcons';
|
||||
import { isValidGjsColor } from 'src/lib/validation/colors';
|
||||
import { AppIconOptions } from './types';
|
||||
import { WorkspaceIconMap } from '../types';
|
||||
import { unique } from 'src/lib/array/helpers';
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const { workspaces, reverse_scroll, ignored } = options.bar.workspaces;
|
||||
const { monochrome, background } = options.theme.bar.buttons;
|
||||
const { background: wsBackground, active } = options.theme.bar.buttons.workspaces;
|
||||
|
||||
const { showWsIcons, showAllActive, numbered_active_indicator: wsActiveIndicator } = options.bar.workspaces;
|
||||
|
||||
/**
|
||||
* A Variable that holds the current map of monitors to the workspace numbers assigned to them.
|
||||
* Determines if a workspace is active on a given monitor.
|
||||
*
|
||||
* This function checks if the workspace with the specified index is currently active on the given monitor.
|
||||
* It uses the `showAllActive` setting and the `hyprlandService` to determine the active workspace on the monitor.
|
||||
*
|
||||
* @param monitor The index of the monitor to check.
|
||||
* @param i The index of the workspace to check.
|
||||
*
|
||||
* @returns True if the workspace is active on the monitor, false otherwise.
|
||||
*/
|
||||
export const workspaceRules = Variable(getWorkspaceMonitorMap());
|
||||
|
||||
/**
|
||||
* A Variable used to force UI or other updates when relevant workspace events occur.
|
||||
*/
|
||||
export const forceUpdater = Variable(true);
|
||||
|
||||
/**
|
||||
* Retrieves the workspace numbers associated with a specific monitor.
|
||||
*
|
||||
* If only one monitor exists, this will simply return a list of all possible workspaces.
|
||||
* Otherwise, it will consult the workspace rules to determine which workspace numbers
|
||||
* belong to the specified monitor.
|
||||
*
|
||||
* @param monitorId - The numeric identifier of the monitor.
|
||||
*
|
||||
* @returns An array of workspace numbers belonging to the specified monitor.
|
||||
*/
|
||||
export function getWorkspacesForMonitor(monitorId: number): number[] {
|
||||
const allMonitors = hyprlandService.get_monitors();
|
||||
|
||||
if (allMonitors.length === 1) {
|
||||
return Array.from({ length: workspaces.get() }, (_, index) => index + 1);
|
||||
}
|
||||
|
||||
const workspaceMonitorRules = getWorkspaceMonitorMap();
|
||||
|
||||
const monitorNameMap: MonitorMap = {};
|
||||
allMonitors.forEach((monitorInstance) => {
|
||||
monitorNameMap[monitorInstance.id] = monitorInstance.name;
|
||||
});
|
||||
|
||||
const currentMonitorName = monitorNameMap[monitorId];
|
||||
|
||||
return workspaceMonitorRules[currentMonitorName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given workspace is valid (assigned) for the specified monitor.
|
||||
*
|
||||
* This function inspects the workspace rules object to determine if the current workspace belongs
|
||||
* to the target monitor. If no workspace rules exist, the function defaults to returning `true`.
|
||||
*
|
||||
* @param workspaceId - The number representing the current workspace.
|
||||
* @param workspaceMonitorRules - The map of monitor names to assigned workspace numbers.
|
||||
* @param monitorId - The numeric identifier for the monitor.
|
||||
* @param workspaceList - A list of Hyprland workspace objects.
|
||||
* @param monitorList - A list of Hyprland monitor objects.
|
||||
*
|
||||
* @returns `true` if the workspace is assigned to the monitor or if no rules exist. Otherwise, `false`.
|
||||
*/
|
||||
function isWorkspaceValidForMonitor(
|
||||
workspaceId: number,
|
||||
workspaceMonitorRules: WorkspaceMonitorMap,
|
||||
monitorId: number,
|
||||
workspaceList: AstalHyprland.Workspace[],
|
||||
monitorList: AstalHyprland.Monitor[],
|
||||
): boolean {
|
||||
const monitorNameMap: MonitorMap = {};
|
||||
const allWorkspaceInstances = workspaceList ?? [];
|
||||
|
||||
const workspaceMonitorReferences = allWorkspaceInstances
|
||||
.filter((workspaceInstance) => workspaceInstance !== null)
|
||||
.map((workspaceInstance) => {
|
||||
return {
|
||||
id: workspaceInstance.monitor?.id,
|
||||
name: workspaceInstance.monitor?.name,
|
||||
};
|
||||
});
|
||||
|
||||
const mergedMonitorInstances = [
|
||||
...new Map(
|
||||
[...workspaceMonitorReferences, ...monitorList].map((monitorCandidate) => [
|
||||
monitorCandidate.id,
|
||||
monitorCandidate,
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
|
||||
mergedMonitorInstances.forEach((monitorInstance) => {
|
||||
monitorNameMap[monitorInstance.id] = monitorInstance.name;
|
||||
});
|
||||
|
||||
const currentMonitorName = monitorNameMap[monitorId];
|
||||
const currentMonitorWorkspaceRules = workspaceMonitorRules[currentMonitorName] ?? [];
|
||||
const activeWorkspaceIds = new Set(allWorkspaceInstances.map((ws) => ws.id));
|
||||
const filteredWorkspaceRules = currentMonitorWorkspaceRules.filter((ws) => !activeWorkspaceIds.has(ws));
|
||||
|
||||
if (filteredWorkspaceRules === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filteredWorkspaceRules.includes(workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a map of monitors to the workspace numbers that belong to them.
|
||||
*
|
||||
* This function communicates with the Hyprland service to retrieve workspace rules in JSON format.
|
||||
* Those rules are parsed, and a map of monitor names to lists of assigned workspace numbers is constructed.
|
||||
*
|
||||
* @returns An object where each key is a monitor name, and each value is an array of workspace numbers.
|
||||
*/
|
||||
function getWorkspaceMonitorMap(): WorkspaceMonitorMap {
|
||||
try {
|
||||
const rulesResponse = hyprlandService.message('j/workspacerules');
|
||||
const workspaceMonitorRules: WorkspaceMonitorMap = {};
|
||||
const parsedWorkspaceRules = JSON.parse(rulesResponse);
|
||||
|
||||
parsedWorkspaceRules.forEach((rule: WorkspaceRule) => {
|
||||
const workspaceNumber = parseInt(rule.workspaceString, 10);
|
||||
|
||||
if (rule.monitor === undefined || isNaN(workspaceNumber)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doesMonitorExistInRules = Object.hasOwnProperty.call(workspaceMonitorRules, rule.monitor);
|
||||
|
||||
if (doesMonitorExistInRules) {
|
||||
workspaceMonitorRules[rule.monitor].push(workspaceNumber);
|
||||
} else {
|
||||
workspaceMonitorRules[rule.monitor] = [workspaceNumber];
|
||||
}
|
||||
});
|
||||
|
||||
return workspaceMonitorRules;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a workspace number should be ignored based on a regular expression.
|
||||
*
|
||||
* @param ignoredWorkspacesVariable - A Variable object containing a string pattern of ignored workspaces.
|
||||
* @param workspaceNumber - The numeric representation of the workspace to check.
|
||||
*
|
||||
* @returns `true` if the workspace should be ignored, otherwise `false`.
|
||||
*/
|
||||
function isWorkspaceIgnored(ignoredWorkspacesVariable: Variable<string>, workspaceNumber: number): boolean {
|
||||
if (ignoredWorkspacesVariable.get() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ignoredWorkspacesRegex = new RegExp(ignoredWorkspacesVariable.get());
|
||||
return ignoredWorkspacesRegex.test(workspaceNumber.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active workspace in the specified direction ('next' or 'prev').
|
||||
*
|
||||
* This function uses the current monitor's set of active or assigned workspaces and
|
||||
* cycles through them in the chosen direction. It also respects the list of ignored
|
||||
* workspaces, skipping any that match the ignored pattern.
|
||||
*
|
||||
* @param direction - The direction to navigate ('next' or 'prev').
|
||||
* @param currentMonitorWorkspacesVariable - A Variable containing an array of workspace numbers for the current monitor.
|
||||
* @param onlyActiveWorkspaces - Whether to only include active (occupied) workspaces when navigating.
|
||||
* @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern.
|
||||
*/
|
||||
function navigateWorkspace(direction: 'next' | 'prev', ignoredWorkspacesVariable: Variable<string>): void {
|
||||
const allHyprlandWorkspaces = hyprlandService.get_workspaces() ?? [];
|
||||
|
||||
const activeWorkspaceIds = allHyprlandWorkspaces
|
||||
.filter((workspaceInstance) => hyprlandService.focusedMonitor.id === workspaceInstance.monitor?.id)
|
||||
.map((workspaceInstance) => workspaceInstance.id);
|
||||
|
||||
const assignedOrOccupiedWorkspaces = activeWorkspaceIds.sort((a, b) => a - b);
|
||||
|
||||
if (assignedOrOccupiedWorkspaces.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const workspaceIndex = assignedOrOccupiedWorkspaces.indexOf(hyprlandService.focusedWorkspace?.id);
|
||||
const step = direction === 'next' ? 1 : -1;
|
||||
|
||||
let newIndex =
|
||||
(workspaceIndex + step + assignedOrOccupiedWorkspaces.length) % assignedOrOccupiedWorkspaces.length;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < assignedOrOccupiedWorkspaces.length) {
|
||||
const targetWorkspaceNumber = assignedOrOccupiedWorkspaces[newIndex];
|
||||
if (!isWorkspaceIgnored(ignoredWorkspacesVariable, targetWorkspaceNumber)) {
|
||||
hyprlandService.dispatch('workspace', targetWorkspaceNumber.toString());
|
||||
return;
|
||||
}
|
||||
newIndex =
|
||||
(newIndex + step + assignedOrOccupiedWorkspaces.length) % assignedOrOccupiedWorkspaces.length;
|
||||
attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the next workspace in the current monitor.
|
||||
*
|
||||
* @param currentMonitorWorkspacesVariable - A Variable containing workspace numbers for the current monitor.
|
||||
* @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces.
|
||||
* @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern.
|
||||
*/
|
||||
export function goToNextWorkspace(ignoredWorkspacesVariable: Variable<string>): void {
|
||||
navigateWorkspace('next', ignoredWorkspacesVariable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the previous workspace in the current monitor.
|
||||
*
|
||||
* @param currentMonitorWorkspacesVariable - A Variable containing workspace numbers for the current monitor.
|
||||
* @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces.
|
||||
* @param ignoredWorkspacesVariable - A Variable that contains the ignored workspaces pattern.
|
||||
*/
|
||||
export function goToPreviousWorkspace(ignoredWorkspacesVariable: Variable<string>): void {
|
||||
navigateWorkspace('prev', ignoredWorkspacesVariable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the execution rate of a given function to prevent it from being called too often.
|
||||
*
|
||||
* @param func - The function to be throttled.
|
||||
* @param limit - The time limit (in milliseconds) during which calls to `func` are disallowed after the first call.
|
||||
*
|
||||
* @returns The throttled version of the input function.
|
||||
*/
|
||||
export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T {
|
||||
let isThrottleActive: boolean;
|
||||
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!isThrottleActive) {
|
||||
func.apply(this, args);
|
||||
isThrottleActive = true;
|
||||
|
||||
setTimeout(() => {
|
||||
isThrottleActive = false;
|
||||
}, limit);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates throttled scroll handlers that navigate workspaces upon scrolling, respecting the configured scroll speed.
|
||||
*
|
||||
* @param scrollSpeed - The factor by which the scroll navigation is throttled.
|
||||
* @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces.
|
||||
*
|
||||
* @returns An object containing two functions (`throttledScrollUp` and `throttledScrollDown`), both throttled.
|
||||
*/
|
||||
export function initThrottledScrollHandlers(scrollSpeed: number): ThrottledScrollHandlers {
|
||||
const throttledScrollUp = throttle(() => {
|
||||
if (reverse_scroll.get()) {
|
||||
goToPreviousWorkspace(ignored);
|
||||
} else {
|
||||
goToNextWorkspace(ignored);
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
const throttledScrollDown = throttle(() => {
|
||||
if (reverse_scroll.get()) {
|
||||
goToNextWorkspace(ignored);
|
||||
} else {
|
||||
goToPreviousWorkspace(ignored);
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
return { throttledScrollUp, throttledScrollDown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes which workspace numbers should be rendered for a given monitor.
|
||||
*
|
||||
* This function consolidates both active and all possible workspaces (based on rules),
|
||||
* then filters them by the selected monitor if `isMonitorSpecific` is set to `true`.
|
||||
*
|
||||
* @param totalWorkspaces - The total number of workspaces (a fallback if workspace rules are not enforced).
|
||||
* @param workspaceInstances - A list of Hyprland workspace objects.
|
||||
* @param workspaceMonitorRules - The map of monitor names to assigned workspace numbers.
|
||||
* @param monitorId - The numeric identifier of the monitor.
|
||||
* @param isMonitorSpecific - If `true`, only include the workspaces that match this monitor.
|
||||
* @param hyprlandMonitorInstances - A list of Hyprland monitor objects.
|
||||
*
|
||||
* @returns An array of workspace numbers that should be shown.
|
||||
*/
|
||||
export function getWorkspacesToRender(
|
||||
totalWorkspaces: number,
|
||||
workspaceInstances: AstalHyprland.Workspace[],
|
||||
workspaceMonitorRules: WorkspaceMonitorMap,
|
||||
monitorId: number,
|
||||
isMonitorSpecific: boolean,
|
||||
hyprlandMonitorInstances: AstalHyprland.Monitor[],
|
||||
): number[] {
|
||||
let allPotentialWorkspaces = range(totalWorkspaces || 8);
|
||||
const allWorkspaceInstances = workspaceInstances ?? [];
|
||||
|
||||
const activeWorkspaceIds = allWorkspaceInstances.map((workspaceInstance) => workspaceInstance.id);
|
||||
|
||||
const monitorReferencesForActiveWorkspaces = allWorkspaceInstances.map((workspaceInstance) => {
|
||||
return {
|
||||
id: workspaceInstance.monitor?.id ?? -1,
|
||||
name: workspaceInstance.monitor?.name ?? '',
|
||||
};
|
||||
});
|
||||
|
||||
const currentMonitorInstance =
|
||||
hyprlandMonitorInstances.find((monitorObj) => monitorObj.id === monitorId) ||
|
||||
monitorReferencesForActiveWorkspaces.find((monitorObj) => monitorObj.id === monitorId);
|
||||
|
||||
const allWorkspacesWithRules = Object.keys(workspaceMonitorRules).reduce(
|
||||
(accumulator: number[], monitorName: string) => {
|
||||
return [...accumulator, ...workspaceMonitorRules[monitorName]];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const activeWorkspacesForCurrentMonitor = activeWorkspaceIds.filter((workspaceId) => {
|
||||
const metadataForWorkspace = allWorkspaceInstances.find(
|
||||
(workspaceObj) => workspaceObj.id === workspaceId,
|
||||
);
|
||||
|
||||
if (metadataForWorkspace) {
|
||||
return metadataForWorkspace?.monitor?.id === monitorId;
|
||||
}
|
||||
|
||||
if (
|
||||
currentMonitorInstance &&
|
||||
Object.hasOwnProperty.call(workspaceMonitorRules, currentMonitorInstance.name) &&
|
||||
allWorkspacesWithRules.includes(workspaceId)
|
||||
) {
|
||||
return workspaceMonitorRules[currentMonitorInstance.name].includes(workspaceId);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isMonitorSpecific) {
|
||||
const validWorkspaceNumbers = range(totalWorkspaces).filter((workspaceNumber) => {
|
||||
return isWorkspaceValidForMonitor(
|
||||
workspaceNumber,
|
||||
workspaceMonitorRules,
|
||||
monitorId,
|
||||
allWorkspaceInstances,
|
||||
hyprlandMonitorInstances,
|
||||
);
|
||||
});
|
||||
|
||||
allPotentialWorkspaces = [
|
||||
...new Set([...activeWorkspacesForCurrentMonitor, ...validWorkspaceNumbers]),
|
||||
];
|
||||
} else {
|
||||
allPotentialWorkspaces = [...new Set([...allPotentialWorkspaces, ...activeWorkspaceIds])];
|
||||
}
|
||||
|
||||
return allPotentialWorkspaces
|
||||
.filter((workspace) => !isWorkspaceIgnored(ignored, workspace))
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to Hyprland service events related to workspaces to keep the local state updated.
|
||||
*
|
||||
* When certain events occur (like a configuration reload or a client being moved/added/removed),
|
||||
* this function updates the workspace rules or toggles the `forceUpdater` variable to ensure
|
||||
* that any dependent UI or logic is re-rendered or re-run.
|
||||
*/
|
||||
export function initWorkspaceEvents(): void {
|
||||
hyprlandService.connect('config-reloaded', () => {
|
||||
workspaceRules.set(getWorkspaceMonitorMap());
|
||||
});
|
||||
|
||||
hyprlandService.connect('client-moved', () => {
|
||||
forceUpdater.set(!forceUpdater.get());
|
||||
});
|
||||
|
||||
hyprlandService.connect('client-added', () => {
|
||||
forceUpdater.set(!forceUpdater.get());
|
||||
});
|
||||
|
||||
hyprlandService.connect('client-removed', () => {
|
||||
forceUpdater.set(!forceUpdater.get());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttled scroll handler functions for navigating workspaces.
|
||||
*/
|
||||
type ThrottledScrollHandlers = {
|
||||
/**
|
||||
* Scroll up throttled handler.
|
||||
*/
|
||||
throttledScrollUp: () => void;
|
||||
|
||||
/**
|
||||
* Scroll down throttled handler.
|
||||
*/
|
||||
throttledScrollDown: () => void;
|
||||
const isWorkspaceActiveOnMonitor = (monitor: number, i: number): boolean => {
|
||||
return showAllActive.get() && hyprlandService.get_monitor(monitor)?.activeWorkspace?.id === i;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the icon for a given workspace.
|
||||
*
|
||||
* This function returns the icon associated with a workspace from the provided workspace icon map.
|
||||
* If no icon is found, it returns the workspace index as a string.
|
||||
*
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icons or icon objects.
|
||||
* @param i The index of the workspace for which to retrieve the icon.
|
||||
*
|
||||
* @returns The icon for the workspace as a string. If no icon is found, returns the workspace index as a string.
|
||||
*/
|
||||
const getWsIcon = (wsIconMap: WorkspaceIconMap, i: number): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
const defaultIcon = `${i}`;
|
||||
|
||||
if (iconEntry === undefined) {
|
||||
return defaultIcon;
|
||||
}
|
||||
|
||||
if (typeof iconEntry === 'string' && iconEntry !== '') {
|
||||
return iconEntry;
|
||||
}
|
||||
|
||||
const hasIcon = typeof iconEntry === 'object' && 'icon' in iconEntry && iconEntry.icon !== '';
|
||||
|
||||
if (hasIcon) {
|
||||
return iconEntry.icon;
|
||||
}
|
||||
|
||||
return defaultIcon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the color for a given workspace.
|
||||
*
|
||||
* This function determines the color styling for a workspace based on the provided workspace icon map,
|
||||
* smart highlighting settings, and the monitor index. It returns a CSS string for the color and background.
|
||||
*
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icon objects.
|
||||
* @param i The index of the workspace for which to retrieve the color.
|
||||
* @param smartHighlight A boolean indicating whether smart highlighting is enabled.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
*
|
||||
* @returns A CSS string representing the color and background for the workspace. If no color is found, returns an empty string.
|
||||
*/
|
||||
export const getWsColor = (
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
const hasColor =
|
||||
typeof iconEntry === 'object' && 'color' in iconEntry && isValidGjsColor(iconEntry.color);
|
||||
|
||||
if (iconEntry === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
showWsIcons.get() &&
|
||||
smartHighlight &&
|
||||
wsActiveIndicator.get() === 'highlight' &&
|
||||
(hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i))
|
||||
) {
|
||||
const iconColor = monochrome.get() ? background.get() : wsBackground.get();
|
||||
const iconBackground = hasColor && isValidGjsColor(iconEntry.color) ? iconEntry.color : active.get();
|
||||
const colorCss = `color: ${iconColor};`;
|
||||
const backgroundCss = `background: ${iconBackground};`;
|
||||
|
||||
return colorCss + backgroundCss;
|
||||
}
|
||||
|
||||
if (hasColor && isValidGjsColor(iconEntry.color)) {
|
||||
return `color: ${iconEntry.color}; border-bottom-color: ${iconEntry.color};`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the application icon for a given workspace.
|
||||
*
|
||||
* This function returns the appropriate application icon for the specified workspace index.
|
||||
* It considers user-defined icons, default icons, and the option to remove duplicate icons.
|
||||
*
|
||||
* @param workspaceIndex The index of the workspace for which to retrieve the application icon.
|
||||
* @param removeDuplicateIcons A boolean indicating whether to remove duplicate icons.
|
||||
* @param options An object containing user-defined icon map, default icon, and empty icon.
|
||||
*
|
||||
* @returns The application icon for the workspace as a string. If no icons are found, returns the default or empty icon.
|
||||
*/
|
||||
export const getAppIcon = (
|
||||
workspaceIndex: number,
|
||||
removeDuplicateIcons: boolean,
|
||||
{ iconMap: userDefinedIconMap, defaultIcon, emptyIcon }: AppIconOptions,
|
||||
): string => {
|
||||
const workspaceClients = hyprlandService
|
||||
.get_clients()
|
||||
.filter((client) => client?.workspace?.id === workspaceIndex)
|
||||
.map((client) => [client.class, client.title]);
|
||||
|
||||
if (!workspaceClients.length) {
|
||||
return emptyIcon;
|
||||
}
|
||||
|
||||
const findIconForClient = (clientClass: string, clientTitle: string): string | undefined => {
|
||||
const appIconMap = { ...userDefinedIconMap, ...defaultApplicationIconMap };
|
||||
|
||||
const iconEntry = Object.entries(appIconMap).find(([matcher]) => {
|
||||
if (matcher.startsWith('class:')) {
|
||||
return new RegExp(matcher.substring(6)).test(clientClass);
|
||||
}
|
||||
|
||||
if (matcher.startsWith('title:')) {
|
||||
return new RegExp(matcher.substring(6)).test(clientTitle);
|
||||
}
|
||||
|
||||
return new RegExp(matcher, 'i').test(clientClass);
|
||||
});
|
||||
|
||||
return iconEntry?.[1] ?? defaultIcon;
|
||||
};
|
||||
|
||||
let icons = workspaceClients.reduce((iconAccumulator, [clientClass, clientTitle]) => {
|
||||
const icon = findIconForClient(clientClass, clientTitle);
|
||||
|
||||
if (icon !== undefined) {
|
||||
iconAccumulator.push(icon);
|
||||
}
|
||||
|
||||
return iconAccumulator;
|
||||
}, []);
|
||||
|
||||
if (icons.length) {
|
||||
if (removeDuplicateIcons) {
|
||||
icons = unique(icons);
|
||||
}
|
||||
|
||||
return icons.join(' ');
|
||||
}
|
||||
|
||||
return defaultIcon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the class names for a workspace.
|
||||
*
|
||||
* This function generates the appropriate class names for a workspace based on various settings such as
|
||||
* whether to show icons, numbered workspaces, workspace icons, and smart highlighting.
|
||||
*
|
||||
* @param showIcons A boolean indicating whether to show icons.
|
||||
* @param showNumbered A boolean indicating whether to show numbered workspaces.
|
||||
* @param numberedActiveIndicator The indicator for active numbered workspaces.
|
||||
* @param showWsIcons A boolean indicating whether to show workspace icons.
|
||||
* @param smartHighlight A boolean indicating whether smart highlighting is enabled.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
* @param i The index of the workspace for which to render class names.
|
||||
*
|
||||
* @returns The class names for the workspace as a string.
|
||||
*/
|
||||
export const renderClassnames = (
|
||||
showIcons: boolean,
|
||||
showNumbered: boolean,
|
||||
numberedActiveIndicator: string,
|
||||
showWsIcons: boolean,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
i: number,
|
||||
): string => {
|
||||
const isWorkspaceActive =
|
||||
hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i);
|
||||
const isActive = isWorkspaceActive ? 'active' : '';
|
||||
|
||||
if (showIcons) {
|
||||
return `workspace-icon txt-icon bar ${isActive}`;
|
||||
}
|
||||
|
||||
if (showNumbered || showWsIcons) {
|
||||
const numActiveInd = isWorkspaceActive ? numberedActiveIndicator : '';
|
||||
|
||||
const wsIconClass = showWsIcons ? 'txt-icon' : '';
|
||||
const smartHighlightClass = smartHighlight ? 'smart-highlight' : '';
|
||||
|
||||
const className = `workspace-number can_${numberedActiveIndicator} ${numActiveInd} ${wsIconClass} ${smartHighlightClass} ${isActive}`;
|
||||
|
||||
return className.trim();
|
||||
}
|
||||
|
||||
return `default ${isActive}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the label for a workspace.
|
||||
*
|
||||
* This function generates the appropriate label for a workspace based on various settings such as
|
||||
* whether to show icons, application icons, workspace icons, and workspace indicators.
|
||||
*
|
||||
* @param showIcons A boolean indicating whether to show icons.
|
||||
* @param availableIndicator The indicator for available workspaces.
|
||||
* @param activeIndicator The indicator for active workspaces.
|
||||
* @param occupiedIndicator The indicator for occupied workspaces.
|
||||
* @param showAppIcons A boolean indicating whether to show application icons.
|
||||
* @param appIcons The application icons as a string.
|
||||
* @param workspaceMask A boolean indicating whether to mask the workspace.
|
||||
* @param showWorkspaceIcons A boolean indicating whether to show workspace icons.
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icons or icon objects.
|
||||
* @param i The index of the workspace for which to render the label.
|
||||
* @param index The index of the workspace in the list.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
*
|
||||
* @returns The label for the workspace as a string.
|
||||
*/
|
||||
export const renderLabel = (
|
||||
showIcons: boolean,
|
||||
availableIndicator: string,
|
||||
activeIndicator: string,
|
||||
occupiedIndicator: string,
|
||||
showAppIcons: boolean,
|
||||
appIcons: string,
|
||||
workspaceMask: boolean,
|
||||
showWorkspaceIcons: boolean,
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
index: number,
|
||||
monitor: number,
|
||||
): string => {
|
||||
if (showAppIcons) {
|
||||
return appIcons;
|
||||
}
|
||||
|
||||
if (showIcons) {
|
||||
if (hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i)) {
|
||||
return activeIndicator;
|
||||
}
|
||||
if ((hyprlandService.get_workspace(i)?.get_clients().length || 0) > 0) {
|
||||
return occupiedIndicator;
|
||||
}
|
||||
if (monitor !== -1) {
|
||||
return availableIndicator;
|
||||
}
|
||||
}
|
||||
|
||||
if (showWorkspaceIcons) {
|
||||
return getWsIcon(wsIconMap, i);
|
||||
}
|
||||
|
||||
return workspaceMask ? `${index + 1}` : `${i}`;
|
||||
};
|
||||
|
||||
7
src/components/bar/modules/workspaces/helpers/types.ts
Normal file
7
src/components/bar/modules/workspaces/helpers/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { ApplicationIcons } from '../types';
|
||||
|
||||
export type AppIconOptions = {
|
||||
iconMap: ApplicationIcons;
|
||||
defaultIcon: string;
|
||||
emptyIcon: string;
|
||||
};
|
||||
@@ -1,276 +1,99 @@
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { defaultApplicationIconMap } from 'src/lib/constants/appIcons';
|
||||
import { WorkspaceIconMap, AppIconOptions } from 'src/lib/types/workspace.types';
|
||||
import { isValidGjsColor } from 'src/lib/utils';
|
||||
import options from 'src/options';
|
||||
import options from 'src/configuration';
|
||||
import { WorkspaceService } from 'src/services/workspace';
|
||||
|
||||
const workspaceService = WorkspaceService.getInstance();
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const { monochrome, background } = options.theme.bar.buttons;
|
||||
const { background: wsBackground, active } = options.theme.bar.buttons.workspaces;
|
||||
|
||||
const { showWsIcons, showAllActive, numbered_active_indicator: wsActiveIndicator } = options.bar.workspaces;
|
||||
const { reverse_scroll } = options.bar.workspaces;
|
||||
|
||||
/**
|
||||
* Determines if a workspace is active on a given monitor.
|
||||
* Limits the execution rate of a given function to prevent it from being called too often.
|
||||
*
|
||||
* This function checks if the workspace with the specified index is currently active on the given monitor.
|
||||
* It uses the `showAllActive` setting and the `hyprlandService` to determine the active workspace on the monitor.
|
||||
* @param func - The function to be throttled.
|
||||
* @param limit - The time limit (in milliseconds) during which calls to `func` are disallowed after the first call.
|
||||
*
|
||||
* @param monitor The index of the monitor to check.
|
||||
* @param i The index of the workspace to check.
|
||||
*
|
||||
* @returns True if the workspace is active on the monitor, false otherwise.
|
||||
* @returns The throttled version of the input function.
|
||||
*/
|
||||
const isWorkspaceActiveOnMonitor = (monitor: number, i: number): boolean => {
|
||||
return showAllActive.get() && hyprlandService.get_monitor(monitor)?.activeWorkspace?.id === i;
|
||||
};
|
||||
function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T {
|
||||
let isThrottleActive: boolean;
|
||||
|
||||
/**
|
||||
* Retrieves the icon for a given workspace.
|
||||
*
|
||||
* This function returns the icon associated with a workspace from the provided workspace icon map.
|
||||
* If no icon is found, it returns the workspace index as a string.
|
||||
*
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icons or icon objects.
|
||||
* @param i The index of the workspace for which to retrieve the icon.
|
||||
*
|
||||
* @returns The icon for the workspace as a string. If no icon is found, returns the workspace index as a string.
|
||||
*/
|
||||
const getWsIcon = (wsIconMap: WorkspaceIconMap, i: number): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
const defaultIcon = `${i}`;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!isThrottleActive) {
|
||||
func.apply(this, args);
|
||||
isThrottleActive = true;
|
||||
|
||||
if (iconEntry === undefined) {
|
||||
return defaultIcon;
|
||||
}
|
||||
|
||||
if (typeof iconEntry === 'string' && iconEntry !== '') {
|
||||
return iconEntry;
|
||||
}
|
||||
|
||||
const hasIcon = typeof iconEntry === 'object' && 'icon' in iconEntry && iconEntry.icon !== '';
|
||||
|
||||
if (hasIcon) {
|
||||
return iconEntry.icon;
|
||||
}
|
||||
|
||||
return defaultIcon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the color for a given workspace.
|
||||
*
|
||||
* This function determines the color styling for a workspace based on the provided workspace icon map,
|
||||
* smart highlighting settings, and the monitor index. It returns a CSS string for the color and background.
|
||||
*
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icon objects.
|
||||
* @param i The index of the workspace for which to retrieve the color.
|
||||
* @param smartHighlight A boolean indicating whether smart highlighting is enabled.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
*
|
||||
* @returns A CSS string representing the color and background for the workspace. If no color is found, returns an empty string.
|
||||
*/
|
||||
export const getWsColor = (
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
const hasColor =
|
||||
typeof iconEntry === 'object' && 'color' in iconEntry && isValidGjsColor(iconEntry.color);
|
||||
|
||||
if (iconEntry === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
showWsIcons.get() &&
|
||||
smartHighlight &&
|
||||
wsActiveIndicator.get() === 'highlight' &&
|
||||
(hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i))
|
||||
) {
|
||||
const iconColor = monochrome.get() ? background.get() : wsBackground.get();
|
||||
const iconBackground = hasColor && isValidGjsColor(iconEntry.color) ? iconEntry.color : active.get();
|
||||
const colorCss = `color: ${iconColor};`;
|
||||
const backgroundCss = `background: ${iconBackground};`;
|
||||
|
||||
return colorCss + backgroundCss;
|
||||
}
|
||||
|
||||
if (hasColor && isValidGjsColor(iconEntry.color)) {
|
||||
return `color: ${iconEntry.color}; border-bottom-color: ${iconEntry.color};`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the application icon for a given workspace.
|
||||
*
|
||||
* This function returns the appropriate application icon for the specified workspace index.
|
||||
* It considers user-defined icons, default icons, and the option to remove duplicate icons.
|
||||
*
|
||||
* @param workspaceIndex The index of the workspace for which to retrieve the application icon.
|
||||
* @param removeDuplicateIcons A boolean indicating whether to remove duplicate icons.
|
||||
* @param options An object containing user-defined icon map, default icon, and empty icon.
|
||||
*
|
||||
* @returns The application icon for the workspace as a string. If no icons are found, returns the default or empty icon.
|
||||
*/
|
||||
export const getAppIcon = (
|
||||
workspaceIndex: number,
|
||||
removeDuplicateIcons: boolean,
|
||||
{ iconMap: userDefinedIconMap, defaultIcon, emptyIcon }: AppIconOptions,
|
||||
): string => {
|
||||
const workspaceClients = hyprlandService
|
||||
.get_clients()
|
||||
.filter((client) => client?.workspace?.id === workspaceIndex)
|
||||
.map((client) => [client.class, client.title]);
|
||||
|
||||
if (!workspaceClients.length) {
|
||||
return emptyIcon;
|
||||
}
|
||||
|
||||
const findIconForClient = (clientClass: string, clientTitle: string): string | undefined => {
|
||||
const appIconMap = { ...userDefinedIconMap, ...defaultApplicationIconMap };
|
||||
|
||||
const iconEntry = Object.entries(appIconMap).find(([matcher]) => {
|
||||
if (matcher.startsWith('class:')) {
|
||||
return new RegExp(matcher.substring(6)).test(clientClass);
|
||||
}
|
||||
|
||||
if (matcher.startsWith('title:')) {
|
||||
return new RegExp(matcher.substring(6)).test(clientTitle);
|
||||
}
|
||||
|
||||
return new RegExp(matcher, 'i').test(clientClass);
|
||||
});
|
||||
|
||||
return iconEntry?.[1] ?? defaultIcon;
|
||||
};
|
||||
|
||||
let icons = workspaceClients.reduce((iconAccumulator, [clientClass, clientTitle]) => {
|
||||
const icon = findIconForClient(clientClass, clientTitle);
|
||||
|
||||
if (icon !== undefined) {
|
||||
iconAccumulator.push(icon);
|
||||
setTimeout(() => {
|
||||
isThrottleActive = false;
|
||||
}, limit);
|
||||
}
|
||||
|
||||
return iconAccumulator;
|
||||
}, []);
|
||||
|
||||
if (icons.length) {
|
||||
if (removeDuplicateIcons) {
|
||||
icons = [...new Set(icons)];
|
||||
}
|
||||
|
||||
return icons.join(' ');
|
||||
}
|
||||
|
||||
return defaultIcon;
|
||||
};
|
||||
} as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the class names for a workspace.
|
||||
* Creates throttled scroll handlers that navigate workspaces upon scrolling, respecting the configured scroll speed.
|
||||
*
|
||||
* This function generates the appropriate class names for a workspace based on various settings such as
|
||||
* whether to show icons, numbered workspaces, workspace icons, and smart highlighting.
|
||||
* @param scrollSpeed - The factor by which the scroll navigation is throttled.
|
||||
* @param onlyActiveWorkspaces - Whether to only navigate among active (occupied) workspaces.
|
||||
*
|
||||
* @param showIcons A boolean indicating whether to show icons.
|
||||
* @param showNumbered A boolean indicating whether to show numbered workspaces.
|
||||
* @param numberedActiveIndicator The indicator for active numbered workspaces.
|
||||
* @param showWsIcons A boolean indicating whether to show workspace icons.
|
||||
* @param smartHighlight A boolean indicating whether smart highlighting is enabled.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
* @param i The index of the workspace for which to render class names.
|
||||
*
|
||||
* @returns The class names for the workspace as a string.
|
||||
* @returns An object containing two functions (`throttledScrollUp` and `throttledScrollDown`), both throttled.
|
||||
*/
|
||||
export const renderClassnames = (
|
||||
showIcons: boolean,
|
||||
showNumbered: boolean,
|
||||
numberedActiveIndicator: string,
|
||||
showWsIcons: boolean,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
i: number,
|
||||
): string => {
|
||||
const isWorkspaceActive =
|
||||
hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i);
|
||||
const isActive = isWorkspaceActive ? 'active' : '';
|
||||
export function initThrottledScrollHandlers(scrollSpeed: number): ThrottledScrollHandlers {
|
||||
const throttledScrollUp = throttle(() => {
|
||||
if (reverse_scroll.get()) {
|
||||
workspaceService.goToPreviousWorkspace();
|
||||
} else {
|
||||
workspaceService.goToNextWorkspace();
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
if (showIcons) {
|
||||
return `workspace-icon txt-icon bar ${isActive}`;
|
||||
}
|
||||
const throttledScrollDown = throttle(() => {
|
||||
if (reverse_scroll.get()) {
|
||||
workspaceService.goToNextWorkspace();
|
||||
} else {
|
||||
workspaceService.goToPreviousWorkspace();
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
if (showNumbered || showWsIcons) {
|
||||
const numActiveInd = isWorkspaceActive ? numberedActiveIndicator : '';
|
||||
|
||||
const wsIconClass = showWsIcons ? 'txt-icon' : '';
|
||||
const smartHighlightClass = smartHighlight ? 'smart-highlight' : '';
|
||||
|
||||
const className = `workspace-number can_${numberedActiveIndicator} ${numActiveInd} ${wsIconClass} ${smartHighlightClass} ${isActive}`;
|
||||
|
||||
return className.trim();
|
||||
}
|
||||
|
||||
return `default ${isActive}`;
|
||||
};
|
||||
return { throttledScrollUp, throttledScrollDown };
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the label for a workspace.
|
||||
* Subscribes to Hyprland service events related to workspaces to keep the local state updated.
|
||||
*
|
||||
* This function generates the appropriate label for a workspace based on various settings such as
|
||||
* whether to show icons, application icons, workspace icons, and workspace indicators.
|
||||
*
|
||||
* @param showIcons A boolean indicating whether to show icons.
|
||||
* @param availableIndicator The indicator for available workspaces.
|
||||
* @param activeIndicator The indicator for active workspaces.
|
||||
* @param occupiedIndicator The indicator for occupied workspaces.
|
||||
* @param showAppIcons A boolean indicating whether to show application icons.
|
||||
* @param appIcons The application icons as a string.
|
||||
* @param workspaceMask A boolean indicating whether to mask the workspace.
|
||||
* @param showWorkspaceIcons A boolean indicating whether to show workspace icons.
|
||||
* @param wsIconMap The map of workspace icons where keys are workspace indices and values are icons or icon objects.
|
||||
* @param i The index of the workspace for which to render the label.
|
||||
* @param index The index of the workspace in the list.
|
||||
* @param monitor The index of the monitor to check for active workspaces.
|
||||
*
|
||||
* @returns The label for the workspace as a string.
|
||||
* When certain events occur (like a configuration reload or a client being moved/added/removed),
|
||||
* this function updates the workspace rules or toggles the `forceUpdater` variable to ensure
|
||||
* that any dependent UI or logic is re-rendered or re-run.
|
||||
*/
|
||||
export const renderLabel = (
|
||||
showIcons: boolean,
|
||||
availableIndicator: string,
|
||||
activeIndicator: string,
|
||||
occupiedIndicator: string,
|
||||
showAppIcons: boolean,
|
||||
appIcons: string,
|
||||
workspaceMask: boolean,
|
||||
showWorkspaceIcons: boolean,
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
index: number,
|
||||
monitor: number,
|
||||
): string => {
|
||||
if (showAppIcons) {
|
||||
return appIcons;
|
||||
}
|
||||
export function initWorkspaceEvents(): void {
|
||||
hyprlandService.connect('config-reloaded', () => {
|
||||
workspaceService.refreshWorkspaceRules();
|
||||
});
|
||||
|
||||
if (showIcons) {
|
||||
if (hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i)) {
|
||||
return activeIndicator;
|
||||
}
|
||||
if ((hyprlandService.get_workspace(i)?.get_clients().length || 0) > 0) {
|
||||
return occupiedIndicator;
|
||||
}
|
||||
if (monitor !== -1) {
|
||||
return availableIndicator;
|
||||
}
|
||||
}
|
||||
hyprlandService.connect('client-moved', () => {
|
||||
workspaceService.forceAnUpdate();
|
||||
});
|
||||
|
||||
if (showWorkspaceIcons) {
|
||||
return getWsIcon(wsIconMap, i);
|
||||
}
|
||||
hyprlandService.connect('client-added', () => {
|
||||
workspaceService.forceAnUpdate();
|
||||
});
|
||||
|
||||
return workspaceMask ? `${index + 1}` : `${i}`;
|
||||
hyprlandService.connect('client-removed', () => {
|
||||
workspaceService.forceAnUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttled scroll handler functions for navigating workspaces.
|
||||
*/
|
||||
type ThrottledScrollHandlers = {
|
||||
/**
|
||||
* Scroll up throttled handler.
|
||||
*/
|
||||
throttledScrollUp: () => void;
|
||||
|
||||
/**
|
||||
* Scroll down throttled handler.
|
||||
*/
|
||||
throttledScrollDown: () => void;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import options from 'src/options';
|
||||
import { initThrottledScrollHandlers } from './helpers';
|
||||
import { initThrottledScrollHandlers } from './helpers/utils';
|
||||
import { WorkspaceModule } from './workspaces';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal, Gdk } from 'astal/gtk3';
|
||||
import { isScrollDown, isScrollUp } from 'src/lib/utils';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { GtkWidget } from 'src/lib/types/widget.types';
|
||||
import options from 'src/configuration';
|
||||
import { isScrollUp, isScrollDown } from 'src/lib/events/mouse';
|
||||
import { BarBoxChild, GtkWidget } from 'src/components/bar/types';
|
||||
|
||||
const { scroll_speed } = options.bar.workspaces;
|
||||
|
||||
|
||||
15
src/components/bar/modules/workspaces/types.ts
Normal file
15
src/components/bar/modules/workspaces/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type WorkspaceIcons = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export type WorkspaceIconsColored = {
|
||||
[key: string]: {
|
||||
color: string;
|
||||
icon: string;
|
||||
};
|
||||
};
|
||||
export type ApplicationIcons = {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
export type WorkspaceIconMap = WorkspaceIcons | WorkspaceIconsColored;
|
||||
@@ -1,11 +1,14 @@
|
||||
import options from 'src/options';
|
||||
import { forceUpdater, getWorkspacesToRender, initWorkspaceEvents, workspaceRules } from './helpers';
|
||||
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from './helpers/utils';
|
||||
import { initWorkspaceEvents } from './helpers/utils';
|
||||
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from './helpers';
|
||||
import { bind, Variable } from 'astal';
|
||||
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { WorkspaceIconMap, ApplicationIcons } from 'src/lib/types/workspace.types';
|
||||
import { WorkspaceService } from 'src/services/workspace';
|
||||
import options from 'src/configuration';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
import { WorkspaceIconMap, ApplicationIcons } from './types';
|
||||
|
||||
const workspaceService = WorkspaceService.getInstance();
|
||||
|
||||
const hyprlandService = AstalHyprland.get_default();
|
||||
const {
|
||||
@@ -61,8 +64,8 @@ export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element
|
||||
bind(ignored),
|
||||
bind(showAllActive),
|
||||
bind(hyprlandService, 'focusedWorkspace'),
|
||||
bind(workspaceRules),
|
||||
bind(forceUpdater),
|
||||
bind(workspaceService.workspaceRules),
|
||||
bind(workspaceService.forceUpdater),
|
||||
],
|
||||
(
|
||||
isMonitorSpecific: boolean,
|
||||
@@ -88,10 +91,11 @@ export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element
|
||||
clients: AstalHyprland.Client[],
|
||||
monitorList: AstalHyprland.Monitor[],
|
||||
) => {
|
||||
const workspacesToRender = getWorkspacesToRender(
|
||||
const wsRules = workspaceService.workspaceRules.get();
|
||||
const workspacesToRender = workspaceService.getWorkspaces(
|
||||
totalWorkspaces,
|
||||
workspaceList,
|
||||
workspaceRules.get(),
|
||||
wsRules,
|
||||
monitor,
|
||||
isMonitorSpecific,
|
||||
monitorList,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import options from 'src/options';
|
||||
import { inputHandler } from 'src/components/bar/utils/helpers.js';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Astal } from 'astal/gtk3';
|
||||
import { systemTime } from 'src/shared/time';
|
||||
import { systemTime } from 'src/lib/units/time';
|
||||
import { GLib } from 'astal';
|
||||
import { Module } from '../../shared/Module';
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import { Module } from '../../shared/module';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import options from 'src/configuration';
|
||||
import { InputHandlerService } from '../../utils/input/inputHandler';
|
||||
|
||||
const inputHandler = InputHandlerService.getInstance();
|
||||
|
||||
const {
|
||||
format,
|
||||
@@ -51,13 +53,15 @@ export const WorldClock = (): BarBoxChild => {
|
||||
.join(timeDivider),
|
||||
);
|
||||
|
||||
let inputHandlerBindings: Variable<void>;
|
||||
|
||||
const microphoneModule = Module({
|
||||
textIcon: iconBinding(),
|
||||
label: timeBinding(),
|
||||
boxClass: 'worldclock',
|
||||
props: {
|
||||
setup: (self: Astal.Button) => {
|
||||
inputHandler(self, {
|
||||
inputHandlerBindings = inputHandler.attachHandlers(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
@@ -75,6 +79,11 @@ export const WorldClock = (): BarBoxChild => {
|
||||
},
|
||||
});
|
||||
},
|
||||
onDestroy: () => {
|
||||
inputHandlerBindings.drop();
|
||||
timeBinding.drop();
|
||||
iconBinding.drop();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Option } from 'src/components/settings/shared/Option';
|
||||
import { Header } from 'src/components/settings/shared/Header';
|
||||
import options from 'src/options';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import options from 'src/configuration';
|
||||
|
||||
export const CustomModuleSettings = (): JSX.Element => {
|
||||
return (
|
||||
@@ -176,6 +176,12 @@ export const CustomModuleSettings = (): JSX.Element => {
|
||||
|
||||
{/* Storage Section */}
|
||||
<Header title="Storage" />
|
||||
<Option
|
||||
opt={options.bar.customModules.storage.paths}
|
||||
title="Paths to Monitor"
|
||||
subtitle="Paths must be absolute paths"
|
||||
type="object"
|
||||
/>
|
||||
<Option
|
||||
opt={options.theme.bar.buttons.modules.storage.enableBorder}
|
||||
title="Button Border"
|
||||
@@ -194,6 +200,19 @@ export const CustomModuleSettings = (): JSX.Element => {
|
||||
type="enum"
|
||||
enums={['used/total', 'used', 'free', 'percentage']}
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.storage.units}
|
||||
title="Unit of measurement"
|
||||
type="enum"
|
||||
enums={['auto', 'bytes', 'kibibytes', 'mebibytes', 'gibibytes', 'tebibytes']}
|
||||
/>
|
||||
<Option
|
||||
opt={options.bar.customModules.storage.tooltipStyle}
|
||||
title="Tooltip Style"
|
||||
subtitle="Choose how drive information is displayed in the tooltip"
|
||||
type="enum"
|
||||
enums={['percentage-bar', 'tree', 'simple']}
|
||||
/>
|
||||
<Option opt={options.bar.customModules.storage.round} title="Round" type="boolean" />
|
||||
<Option
|
||||
opt={options.bar.customModules.storage.pollingInterval}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Option } from 'src/components/settings/shared/Option';
|
||||
import { Header } from 'src/components/settings/shared/Header';
|
||||
|
||||
import options from 'src/options';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import options from 'src/configuration';
|
||||
|
||||
export const CustomModuleTheme = (): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { BarBoxChild, BarModuleProps } from 'src/lib/types/bar.types';
|
||||
import { BarButtonStyles } from 'src/lib/options/options.types';
|
||||
import options from 'src/options';
|
||||
import { BarBoxChild, BarModuleProps } from 'src/components/bar/types';
|
||||
import { BarButtonStyles } from 'src/lib/options/types';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const { style } = options.theme.bar.buttons;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BarBoxChild } from 'src/lib/types/bar.types';
|
||||
import options from '../../../options';
|
||||
import { BarBoxChild } from 'src/components/bar/types';
|
||||
import { bind, Binding } from 'astal';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const computeVisible = (child: BarBoxChild): Binding<boolean> | boolean => {
|
||||
if (child.isVis !== undefined) {
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Widget } from 'astal/gtk3';
|
||||
import { Binding, Variable } from 'astal';
|
||||
import { Astal, Gdk, Gtk, Widget } from 'astal/gtk3';
|
||||
import { Binding } from 'astal';
|
||||
import { Connectable } from 'astal/binding';
|
||||
import { BoxWidget } from './widget.types';
|
||||
import { Label } from 'astal/gtk3/widget';
|
||||
|
||||
export type BarBoxChild = {
|
||||
@@ -13,8 +12,8 @@ export type BarBoxChild = {
|
||||
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;
|
||||
type BoxHook = (self: Gtk.Box) => void;
|
||||
type LabelHook = (self: Label) => void;
|
||||
|
||||
export type BarModuleProps = {
|
||||
icon?: string | Binding<string>;
|
||||
@@ -35,7 +34,24 @@ export type BarModuleProps = {
|
||||
connection?: Binding<Connectable>;
|
||||
};
|
||||
|
||||
export type ResourceLabelType = 'used/total' | 'used' | 'percentage' | 'free';
|
||||
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 type NetstatLabelType = 'full' | 'in' | 'out';
|
||||
export type RateUnit = 'GiB' | 'MiB' | 'KiB' | 'auto';
|
||||
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,422 +0,0 @@
|
||||
import { bind, Binding, execAsync, Variable } from 'astal';
|
||||
import { openMenu } from 'src/components/bar/utils/menu';
|
||||
import options from 'src/options';
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { onMiddleClick, onPrimaryClick, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { isScrollDown, isScrollUp } from 'src/lib/utils';
|
||||
import { ResourceLabelType } from 'src/lib/types/bar.types';
|
||||
import { UpdateHandlers, Postfix, GenericResourceData } from 'src/lib/types/customModules/generic.types';
|
||||
import {
|
||||
RunAsyncCommand,
|
||||
InputHandlerEvents,
|
||||
InputHandlerEventArgs,
|
||||
} from 'src/lib/types/customModules/utils.types';
|
||||
import { ThrottleFn } from 'src/lib/types/utils.types';
|
||||
import { GtkWidget } from 'src/lib/types/widget.types';
|
||||
|
||||
const { scrollSpeed } = options.bar.customModules;
|
||||
|
||||
const dummyVar = Variable('');
|
||||
|
||||
/**
|
||||
* Handles the post input updater by toggling its value.
|
||||
*
|
||||
* This function checks if the `postInputUpdater` variable is defined. If it is, it toggles its value.
|
||||
*
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
const handlePostInputUpdater = (postInputUpdater?: Variable<boolean>): void => {
|
||||
if (postInputUpdater !== undefined) {
|
||||
postInputUpdater.set(!postInputUpdater.get());
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes an asynchronous command and handles the result.
|
||||
*
|
||||
* This function runs a given command asynchronously using `execAsync`. If the command starts with 'menu:', it opens the specified menu.
|
||||
* Otherwise, it executes the command in a bash shell. After execution, it handles the post input updater and calls the provided callback function with the command output.
|
||||
*
|
||||
* @param cmd The command to execute.
|
||||
* @param events An object containing the clicked widget and event information.
|
||||
* @param fn An optional callback function to handle the command output.
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
export const runAsyncCommand: RunAsyncCommand = (
|
||||
cmd,
|
||||
events,
|
||||
fn,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
): void => {
|
||||
if (cmd.startsWith('menu:')) {
|
||||
const menuName = cmd.split(':')[1].trim().toLowerCase();
|
||||
openMenu(events.clicked, events.event, `${menuName}menu`);
|
||||
handlePostInputUpdater(postInputUpdater);
|
||||
return;
|
||||
}
|
||||
|
||||
execAsync(['bash', '-c', cmd])
|
||||
.then((output) => {
|
||||
handlePostInputUpdater(postInputUpdater);
|
||||
if (fn !== undefined) {
|
||||
fn(output);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(`Error running command "${cmd}": ${err})`));
|
||||
};
|
||||
|
||||
/*
|
||||
* NOTE: Added a throttle since spamming a button yields duplicate events
|
||||
* which undo the toggle.
|
||||
*/
|
||||
const throttledAsyncCommand = throttleInput(
|
||||
(cmd, events, fn, postInputUpdater?: Variable<boolean>) =>
|
||||
runAsyncCommand(cmd, events, fn, postInputUpdater),
|
||||
50,
|
||||
);
|
||||
|
||||
/**
|
||||
* Generic throttle function to limit the rate at which a function can be called.
|
||||
*
|
||||
* This function creates a throttled version of the provided function that can only be called once within the specified limit.
|
||||
*
|
||||
* @param func The function to throttle.
|
||||
* @param limit The time limit in milliseconds.
|
||||
*
|
||||
* @returns The throttled function.
|
||||
*/
|
||||
export function throttleInput<T extends ThrottleFn>(func: T, limit: number): T {
|
||||
let inThrottle = false;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a throttled scroll handler with the given interval.
|
||||
*
|
||||
* This function returns a throttled version of the `runAsyncCommand` function that can be called with the specified interval.
|
||||
*
|
||||
* @param interval The interval in milliseconds.
|
||||
*
|
||||
* @returns The throttled scroll handler function.
|
||||
*/
|
||||
export const throttledScrollHandler = (interval: number): ThrottleFn =>
|
||||
throttleInput((cmd: string, args, fn, postInputUpdater) => {
|
||||
throttledAsyncCommand(cmd, args, fn, postInputUpdater);
|
||||
}, 200 / interval);
|
||||
|
||||
/**
|
||||
* Handles input events for a GtkWidget.
|
||||
*
|
||||
* This function sets up event handlers for primary, secondary, and middle clicks, as well as scroll events.
|
||||
* It uses the provided input handler events and post input updater to manage the input state.
|
||||
*
|
||||
* @param self The GtkWidget instance to handle input events for.
|
||||
* @param inputHandlerEvents An object containing the input handler events for primary, secondary, and middle clicks, as well as scroll up and down.
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
export const inputHandler = (
|
||||
self: GtkWidget,
|
||||
{
|
||||
onPrimaryClick: onPrimaryClickInput,
|
||||
onSecondaryClick: onSecondaryClickInput,
|
||||
onMiddleClick: onMiddleClickInput,
|
||||
onScrollUp: onScrollUpInput,
|
||||
onScrollDown: onScrollDownInput,
|
||||
}: InputHandlerEvents,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
customScrollThreshold?: number,
|
||||
): void => {
|
||||
const sanitizeInput = (input?: Variable<string>): string => {
|
||||
if (input === undefined) {
|
||||
return '';
|
||||
}
|
||||
return input.get();
|
||||
};
|
||||
|
||||
const updateHandlers = (): UpdateHandlers => {
|
||||
const interval = customScrollThreshold ?? scrollSpeed.get();
|
||||
const throttledHandler = throttledScrollHandler(interval);
|
||||
|
||||
const disconnectPrimaryClick = onPrimaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
throttledAsyncCommand(
|
||||
sanitizeInput(onPrimaryClickInput?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onPrimaryClickInput?.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
});
|
||||
|
||||
const disconnectSecondaryClick = onSecondaryClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
throttledAsyncCommand(
|
||||
sanitizeInput(onSecondaryClickInput?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onSecondaryClickInput?.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
});
|
||||
|
||||
const disconnectMiddleClick = onMiddleClick(self, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
throttledAsyncCommand(
|
||||
sanitizeInput(onMiddleClickInput?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onMiddleClickInput?.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
});
|
||||
|
||||
const id = self.connect('scroll-event', (self: GtkWidget, event: Gdk.Event) => {
|
||||
const handleScroll = (input?: InputHandlerEventArgs): void => {
|
||||
if (input) {
|
||||
throttledHandler(
|
||||
sanitizeInput(input.cmd),
|
||||
{ clicked: self, event },
|
||||
input.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isScrollUp(event)) {
|
||||
handleScroll(onScrollUpInput);
|
||||
}
|
||||
|
||||
if (isScrollDown(event)) {
|
||||
handleScroll(onScrollDownInput);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
disconnectPrimary: disconnectPrimaryClick,
|
||||
disconnectSecondary: disconnectSecondaryClick,
|
||||
disconnectMiddle: disconnectMiddleClick,
|
||||
disconnectScroll: () => self.disconnect(id),
|
||||
};
|
||||
};
|
||||
|
||||
updateHandlers();
|
||||
|
||||
const sanitizeVariable = (someVar?: Variable<string>): Binding<string> => {
|
||||
if (someVar === undefined) {
|
||||
return bind(dummyVar);
|
||||
}
|
||||
return bind(someVar);
|
||||
};
|
||||
|
||||
Variable.derive(
|
||||
[
|
||||
bind(scrollSpeed),
|
||||
sanitizeVariable(onPrimaryClickInput?.cmd),
|
||||
sanitizeVariable(onSecondaryClickInput?.cmd),
|
||||
sanitizeVariable(onMiddleClickInput?.cmd),
|
||||
sanitizeVariable(onScrollUpInput?.cmd),
|
||||
sanitizeVariable(onScrollDownInput?.cmd),
|
||||
],
|
||||
() => {
|
||||
const handlers = updateHandlers();
|
||||
|
||||
handlers.disconnectPrimary();
|
||||
handlers.disconnectSecondary();
|
||||
handlers.disconnectMiddle();
|
||||
handlers.disconnectScroll();
|
||||
},
|
||||
)();
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the percentage of used resources.
|
||||
*
|
||||
* This function calculates the percentage of used resources based on the total and used values.
|
||||
* It can optionally round the result to the nearest integer.
|
||||
*
|
||||
* @param totalUsed An array containing the total and used values.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The percentage of used resources as a number.
|
||||
*/
|
||||
export const divide = ([total, used]: number[], round: boolean): number => {
|
||||
const percentageTotal = (used / total) * 100;
|
||||
if (round) {
|
||||
return total > 0 ? Math.round(percentageTotal) : 0;
|
||||
}
|
||||
return total > 0 ? parseFloat(percentageTotal.toFixed(2)) : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to KiB.
|
||||
*
|
||||
* This function converts a size in bytes to kibibytes (KiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in KiB as a number.
|
||||
*/
|
||||
export const formatSizeInKiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 1;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to MiB.
|
||||
*
|
||||
* This function converts a size in bytes to mebibytes (MiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in MiB as a number.
|
||||
*/
|
||||
export const formatSizeInMiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 2;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to GiB.
|
||||
*
|
||||
* This function converts a size in bytes to gibibytes (GiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in GiB as a number.
|
||||
*/
|
||||
export const formatSizeInGiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 3;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a size in bytes to TiB.
|
||||
*
|
||||
* This function converts a size in bytes to tebibytes (TiB) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The size in TiB as a number.
|
||||
*/
|
||||
export const formatSizeInTiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 4;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically formats a size in bytes to the appropriate unit.
|
||||
*
|
||||
* This function converts a size in bytes to the most appropriate unit (TiB, GiB, MiB, KiB, or bytes) and optionally rounds the result.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to format.
|
||||
* @param round A boolean indicating whether to round the result.
|
||||
*
|
||||
* @returns The formatted size as a number.
|
||||
*/
|
||||
export const autoFormatSize = (sizeInBytes: number, round: boolean): number => {
|
||||
// auto convert to GiB, MiB, KiB, TiB, or bytes
|
||||
if (sizeInBytes >= 1024 ** 4) return formatSizeInTiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 3) return formatSizeInGiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 2) return formatSizeInMiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 1) return formatSizeInKiB(sizeInBytes, round);
|
||||
|
||||
return sizeInBytes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the appropriate postfix for a size in bytes.
|
||||
*
|
||||
* This function returns the appropriate postfix (TiB, GiB, MiB, KiB, or B) for a given size in bytes.
|
||||
*
|
||||
* @param sizeInBytes The size in bytes to determine the postfix for.
|
||||
*
|
||||
* @returns The postfix as a string.
|
||||
*/
|
||||
export const getPostfix = (sizeInBytes: number): Postfix => {
|
||||
if (sizeInBytes >= 1024 ** 4) return 'TiB';
|
||||
if (sizeInBytes >= 1024 ** 3) return 'GiB';
|
||||
if (sizeInBytes >= 1024 ** 2) return 'MiB';
|
||||
if (sizeInBytes >= 1024 ** 1) return 'KiB';
|
||||
|
||||
return 'B';
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a resource label based on the label type and resource data.
|
||||
*
|
||||
* This function generates a resource label string based on the provided label type, resource data, and rounding option.
|
||||
* It formats the used, total, and free resource values and calculates the percentage if needed.
|
||||
*
|
||||
* @param lblType The type of label to render (used/total, used, free, or percentage).
|
||||
* @param rmUsg An object containing the resource usage data (used, total, percentage, and free).
|
||||
* @param round A boolean indicating whether to round the values.
|
||||
*
|
||||
* @returns The rendered resource label as a string.
|
||||
*/
|
||||
export const renderResourceLabel = (
|
||||
lblType: ResourceLabelType,
|
||||
rmUsg: GenericResourceData,
|
||||
round: boolean,
|
||||
): string => {
|
||||
const { used, total, percentage, free } = rmUsg;
|
||||
|
||||
const formatFunctions = {
|
||||
TiB: formatSizeInTiB,
|
||||
GiB: formatSizeInGiB,
|
||||
MiB: formatSizeInMiB,
|
||||
KiB: formatSizeInKiB,
|
||||
B: (size: number): number => size,
|
||||
};
|
||||
|
||||
// Get the data in proper GiB, MiB, KiB, TiB, or bytes
|
||||
const totalSizeFormatted = autoFormatSize(total, round);
|
||||
// get the postfix: one of [TiB, GiB, MiB, KiB, B]
|
||||
const postfix = getPostfix(total);
|
||||
|
||||
// Determine which format function to use
|
||||
const formatUsed = formatFunctions[postfix] ?? formatFunctions['B'];
|
||||
const usedSizeFormatted = formatUsed(used, round);
|
||||
|
||||
if (lblType === 'used/total') {
|
||||
return `${usedSizeFormatted}/${totalSizeFormatted} ${postfix}`;
|
||||
}
|
||||
if (lblType === 'used') {
|
||||
return `${autoFormatSize(used, round)} ${getPostfix(used)}`;
|
||||
}
|
||||
if (lblType === 'free') {
|
||||
return `${autoFormatSize(free, round)} ${getPostfix(free)}`;
|
||||
}
|
||||
|
||||
return `${percentage}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a tooltip based on the data type and label type.
|
||||
*
|
||||
* This function generates a tooltip string based on the provided data type and label type.
|
||||
*
|
||||
* @param dataType The type of data to include in the tooltip.
|
||||
* @param lblTyp The type of label to format the tooltip for (used, free, used/total, or percentage).
|
||||
*
|
||||
* @returns The formatted tooltip as a string.
|
||||
*/
|
||||
export const formatTooltip = (dataType: string, lblTyp: ResourceLabelType): string => {
|
||||
switch (lblTyp) {
|
||||
case 'used':
|
||||
return `Used ${dataType}`;
|
||||
case 'free':
|
||||
return `Free ${dataType}`;
|
||||
case 'used/total':
|
||||
return `Used/Total ${dataType}`;
|
||||
case 'percentage':
|
||||
return `Percentage ${dataType} Usage`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
53
src/components/bar/utils/input/commandExecutor.ts
Normal file
53
src/components/bar/utils/input/commandExecutor.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { execAsync, Variable } from 'astal';
|
||||
import { openDropdownMenu } from '../menu';
|
||||
import { EventArgs } from './types';
|
||||
|
||||
/**
|
||||
* Executes an asynchronous command and handles the result.
|
||||
*
|
||||
* This function runs a given command asynchronously using `execAsync`. If the command starts with 'menu:', it opens the specified menu.
|
||||
* Otherwise, it executes the command in a bash shell. After execution, it handles the post input updater and calls the provided callback function with the command output.
|
||||
*
|
||||
* @param cmd The command to execute.
|
||||
* @param events An object containing the clicked widget and event information.
|
||||
* @param fn An optional callback function to handle the command output.
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
export function runAsyncCommand(
|
||||
cmd: string,
|
||||
events: EventArgs,
|
||||
fn?: (output: string) => void,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
): void {
|
||||
if (cmd.startsWith('menu:')) {
|
||||
const menuName = cmd.split(':')[1].trim().toLowerCase();
|
||||
|
||||
openDropdownMenu(events.clicked, events.event, `${menuName}menu`);
|
||||
handlePostInputUpdater(postInputUpdater);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
execAsync(['bash', '-c', cmd])
|
||||
.then((output) => {
|
||||
handlePostInputUpdater(postInputUpdater);
|
||||
|
||||
if (fn !== undefined) {
|
||||
fn(output);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(`Error running command "${cmd}": ${err})`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the post input updater by toggling its value.
|
||||
*
|
||||
* This function checks if the `postInputUpdater` variable is defined. If it is, it toggles its value.
|
||||
*
|
||||
* @param postInputUpdater An optional Variable<boolean> that tracks the post input update state.
|
||||
*/
|
||||
function handlePostInputUpdater(postInputUpdater?: Variable<boolean>): void {
|
||||
if (postInputUpdater !== undefined) {
|
||||
postInputUpdater.set(!postInputUpdater.get());
|
||||
}
|
||||
}
|
||||
228
src/components/bar/utils/input/inputHandler.ts
Normal file
228
src/components/bar/utils/input/inputHandler.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { bind, Binding, Variable } from 'astal';
|
||||
import { onMiddleClick, onPrimaryClick, onSecondaryClick } from 'src/lib/shared/eventHandlers';
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { isScrollDown, isScrollUp } from 'src/lib/events/mouse';
|
||||
import { throttledAsyncCommand, throttledScrollHandler } from './throttle';
|
||||
import options from 'src/configuration';
|
||||
import { InputHandlerEventArgs, InputHandlerEvents, UpdateHandlers } from './types';
|
||||
import { GtkWidget } from '../../types';
|
||||
|
||||
type EventType = 'primary' | 'secondary' | 'middle';
|
||||
type ClickHandler = typeof onPrimaryClick | typeof onSecondaryClick | typeof onMiddleClick;
|
||||
|
||||
interface EventConfig {
|
||||
event?: InputHandlerEventArgs;
|
||||
handler: ClickHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service responsible for managing input userDefinedActions for widgets
|
||||
*/
|
||||
export class InputHandlerService {
|
||||
private static _instance: InputHandlerService;
|
||||
private readonly _EMPTY_CMD = Variable('');
|
||||
private readonly _scrollSpeed = options.bar.customModules.scrollSpeed;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): InputHandlerService {
|
||||
if (this._instance === undefined) {
|
||||
this._instance = new InputHandlerService();
|
||||
}
|
||||
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches input handlers to a widget and manages their lifecycle
|
||||
*/
|
||||
public attachHandlers(
|
||||
widget: GtkWidget,
|
||||
userDefinedActions: InputHandlerEvents,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
customScrollThreshold?: number,
|
||||
): Variable<void> {
|
||||
const eventHandlers = this._createEventHandlers(
|
||||
widget,
|
||||
userDefinedActions,
|
||||
postInputUpdater,
|
||||
customScrollThreshold,
|
||||
);
|
||||
|
||||
return this._setupBindings(
|
||||
widget,
|
||||
userDefinedActions,
|
||||
eventHandlers,
|
||||
postInputUpdater,
|
||||
customScrollThreshold,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates event handlers for the widget
|
||||
*/
|
||||
private _createEventHandlers(
|
||||
widget: GtkWidget,
|
||||
userDefinedActions: InputHandlerEvents,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
customScrollThreshold?: number,
|
||||
): UpdateHandlers {
|
||||
const clickHandlers = this._createClickHandlers(widget, userDefinedActions, postInputUpdater);
|
||||
const scrollHandler = this._createScrollHandler(
|
||||
widget,
|
||||
userDefinedActions,
|
||||
postInputUpdater,
|
||||
customScrollThreshold,
|
||||
);
|
||||
|
||||
return {
|
||||
...clickHandlers,
|
||||
...scrollHandler,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates click event handlers (primary, secondary, middle)
|
||||
*/
|
||||
private _createClickHandlers(
|
||||
widget: GtkWidget,
|
||||
userDefinedActions: InputHandlerEvents,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
): Pick<UpdateHandlers, 'disconnectPrimary' | 'disconnectSecondary' | 'disconnectMiddle'> {
|
||||
const eventConfigs: Record<EventType, EventConfig> = {
|
||||
primary: { event: userDefinedActions.onPrimaryClick, handler: onPrimaryClick },
|
||||
secondary: { event: userDefinedActions.onSecondaryClick, handler: onSecondaryClick },
|
||||
middle: { event: userDefinedActions.onMiddleClick, handler: onMiddleClick },
|
||||
};
|
||||
|
||||
return {
|
||||
disconnectPrimary: this._createClickHandler(widget, eventConfigs.primary, postInputUpdater),
|
||||
disconnectSecondary: this._createClickHandler(widget, eventConfigs.secondary, postInputUpdater),
|
||||
disconnectMiddle: this._createClickHandler(widget, eventConfigs.middle, postInputUpdater),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single click handler
|
||||
*/
|
||||
private _createClickHandler(
|
||||
widget: GtkWidget,
|
||||
config: EventConfig,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
): () => void {
|
||||
return config.handler(widget, (clicked: GtkWidget, event: Gdk.Event) => {
|
||||
throttledAsyncCommand(
|
||||
this._sanitizeInput(config.event?.cmd),
|
||||
{ clicked, event },
|
||||
config.event?.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates scroll event handler
|
||||
*/
|
||||
private _createScrollHandler(
|
||||
widget: GtkWidget,
|
||||
userDefinedActions: InputHandlerEvents,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
customScrollThreshold?: number,
|
||||
): Pick<UpdateHandlers, 'disconnectScroll'> {
|
||||
const interval = customScrollThreshold ?? this._scrollSpeed.get();
|
||||
const throttledHandler = throttledScrollHandler(interval);
|
||||
|
||||
const id = widget.connect('scroll-event', (self: GtkWidget, event: Gdk.Event) => {
|
||||
const scrollAction = this._getScrollAction(event, userDefinedActions);
|
||||
|
||||
if (scrollAction) {
|
||||
throttledHandler(
|
||||
this._sanitizeInput(scrollAction.cmd),
|
||||
{ clicked: self, event },
|
||||
scrollAction.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
disconnectScroll: () => widget.disconnect(id),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which scroll configuration to use based on event
|
||||
*/
|
||||
private _getScrollAction(
|
||||
event: Gdk.Event,
|
||||
userDefinedActions: InputHandlerEvents,
|
||||
): InputHandlerEventArgs | undefined {
|
||||
if (isScrollUp(event)) {
|
||||
return userDefinedActions.onScrollUp;
|
||||
}
|
||||
|
||||
if (isScrollDown(event)) {
|
||||
return userDefinedActions.onScrollDown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up reactive bindings that recreate handlers when dependencies change
|
||||
*/
|
||||
private _setupBindings(
|
||||
widget: GtkWidget,
|
||||
userDefinedActions: InputHandlerEvents,
|
||||
handlers: UpdateHandlers,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
customScrollThreshold?: number,
|
||||
): Variable<void> {
|
||||
const eventCommands = [
|
||||
userDefinedActions.onPrimaryClick?.cmd,
|
||||
userDefinedActions.onSecondaryClick?.cmd,
|
||||
userDefinedActions.onMiddleClick?.cmd,
|
||||
userDefinedActions.onScrollUp?.cmd,
|
||||
userDefinedActions.onScrollDown?.cmd,
|
||||
];
|
||||
|
||||
const eventCommandBindings = eventCommands.map((cmd) => this._sanitizeVariable(cmd));
|
||||
|
||||
return Variable.derive([bind(this._scrollSpeed), ...eventCommandBindings], () => {
|
||||
this._disconnectHandlers(handlers);
|
||||
|
||||
const newHandlers = this._createEventHandlers(
|
||||
widget,
|
||||
userDefinedActions,
|
||||
postInputUpdater,
|
||||
customScrollThreshold,
|
||||
);
|
||||
|
||||
Object.assign(handlers, newHandlers);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects all event handlers
|
||||
*/
|
||||
private _disconnectHandlers(handlers: UpdateHandlers): void {
|
||||
handlers.disconnectPrimary();
|
||||
handlers.disconnectSecondary();
|
||||
handlers.disconnectMiddle();
|
||||
handlers.disconnectScroll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a variable input to a string
|
||||
*/
|
||||
private _sanitizeInput(input?: Variable<string> | undefined): string {
|
||||
if (!input) return '';
|
||||
|
||||
return input.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a variable for binding
|
||||
*/
|
||||
private _sanitizeVariable(variable?: Variable<string> | undefined): Binding<string> {
|
||||
return bind(variable ?? this._EMPTY_CMD);
|
||||
}
|
||||
}
|
||||
50
src/components/bar/utils/input/throttle.ts
Normal file
50
src/components/bar/utils/input/throttle.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Variable } from 'astal';
|
||||
import { runAsyncCommand } from './commandExecutor';
|
||||
import { ThrottleFn } from 'src/lib/shared/eventHandlers/types';
|
||||
|
||||
/**
|
||||
* Generic throttle function to limit the rate at which a function can be called.
|
||||
*
|
||||
* This function creates a throttled version of the provided function that can only be called once within the specified limit.
|
||||
*
|
||||
* @param func The function to throttle.
|
||||
* @param limit The time limit in milliseconds.
|
||||
*
|
||||
* @returns The throttled function.
|
||||
*/
|
||||
export function throttleInput<T extends ThrottleFn>(func: T, limit: number): T {
|
||||
let inThrottle = false;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a throttled scroll handler with the given interval.
|
||||
*
|
||||
* This function returns a throttled version of the `runAsyncCommand` function that can be called with the specified interval.
|
||||
*
|
||||
* @param interval The interval in milliseconds.
|
||||
*
|
||||
* @returns The throttled scroll handler function.
|
||||
*/
|
||||
export const throttledScrollHandler = (interval: number): ThrottleFn =>
|
||||
throttleInput((cmd: string, args, fn, postInputUpdater) => {
|
||||
throttledAsyncCommand(cmd, args, fn, postInputUpdater);
|
||||
}, 200 / interval);
|
||||
|
||||
/*
|
||||
* NOTE: Added a throttle since spamming a button yields duplicate events
|
||||
* which undo the toggle.
|
||||
*/
|
||||
export const throttledAsyncCommand = throttleInput(
|
||||
(cmd, events, fn, postInputUpdater?: Variable<boolean>) =>
|
||||
runAsyncCommand(cmd, events, fn, postInputUpdater),
|
||||
50,
|
||||
);
|
||||
@@ -1,6 +1,19 @@
|
||||
import { Variable } from 'astal';
|
||||
import { EventArgs } from '../widget.types';
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { Opt } from 'src/lib/options';
|
||||
import { GtkWidget } from '../../types';
|
||||
|
||||
export type EventArgs = {
|
||||
clicked: GtkWidget;
|
||||
event: Gdk.Event;
|
||||
};
|
||||
|
||||
export type UpdateHandlers = {
|
||||
disconnectPrimary: () => void;
|
||||
disconnectSecondary: () => void;
|
||||
disconnectMiddle: () => void;
|
||||
disconnectScroll: () => void;
|
||||
};
|
||||
|
||||
export type InputHandlerEventArgs = {
|
||||
cmd?: Opt<string> | Variable<string>;
|
||||
@@ -13,10 +26,3 @@ export type InputHandlerEvents = {
|
||||
onScrollUp?: InputHandlerEventArgs;
|
||||
onScrollDown?: InputHandlerEventArgs;
|
||||
};
|
||||
|
||||
export type RunAsyncCommand = (
|
||||
cmd: string,
|
||||
args: EventArgs,
|
||||
fn?: (output: string) => void,
|
||||
postInputUpdater?: Variable<boolean>,
|
||||
) => void;
|
||||
@@ -1,41 +1,24 @@
|
||||
import { App, Gdk } from 'astal/gtk3';
|
||||
import { GtkWidget } from 'src/lib/types/widget.types';
|
||||
import { calculateMenuPosition } from 'src/components/menus/shared/dropdown/locationHandler';
|
||||
|
||||
export const closeAllMenus = (): void => {
|
||||
const menuWindows = App.get_windows()
|
||||
.filter((w) => {
|
||||
if (w.name) {
|
||||
return /.*menu/.test(w.name);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((window) => window.name);
|
||||
|
||||
menuWindows.forEach((window) => {
|
||||
if (window) {
|
||||
App.get_window(window)?.set_visible(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const openMenu = async (clicked: GtkWidget, event: Gdk.Event, window: string): Promise<void> => {
|
||||
/*
|
||||
* NOTE: We have to make some adjustments so the menu pops up relatively
|
||||
* to the center of the button clicked. We don't want the menu to spawn
|
||||
* offcenter depending on which edge of the button you click on.
|
||||
* -------------
|
||||
* To fix this, we take the x coordinate of the click within the button's bounds.
|
||||
* If you click the left edge of a 100 width button, then the x axis will be 0
|
||||
* and if you click the right edge then the x axis will be 100.
|
||||
* -------------
|
||||
* Then we divide the width of the button by 2 to get the center of the button and then get
|
||||
* the offset by subtracting the clicked x coordinate. Then we can apply that offset
|
||||
* to the x coordinate of the click relative to the screen to get the center of the
|
||||
* icon click.
|
||||
*/
|
||||
import { calculateMenuPosition } from 'src/components/menus/shared/dropdown/helpers/locationHandler';
|
||||
import { GtkWidget } from '../../types';
|
||||
|
||||
/**
|
||||
* Opens a dropdown menu centered relative to the clicked button
|
||||
*
|
||||
* This function handles the positioning logic to ensure menus appear centered
|
||||
* relative to the button that was clicked, regardless of where on the button
|
||||
* the click occurred. It calculates the offset needed to center the menu
|
||||
* based on the click position within the button's bounds.
|
||||
*
|
||||
* @param clicked - The widget that was clicked to trigger the menu
|
||||
* @param event - The click event containing position information
|
||||
* @param window - The name of the menu window to open
|
||||
*/
|
||||
export const openDropdownMenu = async (
|
||||
clicked: GtkWidget,
|
||||
event: Gdk.Event,
|
||||
window: string,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const middleOfButton = Math.floor(clicked.get_allocated_width() / 2);
|
||||
const xAxisOfButtonClick = clicked.get_pointer()[0];
|
||||
@@ -57,3 +40,28 @@ export const openMenu = async (clicked: GtkWidget, event: Gdk.Event, window: str
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes all currently open menu windows
|
||||
*
|
||||
* This function finds all windows whose names contain "menu" and
|
||||
* hides them. It's used to ensure only one menu is open at a time
|
||||
* when opening a new dropdown menu.
|
||||
*/
|
||||
function closeAllMenus(): void {
|
||||
const menuWindows = App.get_windows()
|
||||
.filter((w) => {
|
||||
if (w.name) {
|
||||
return /.*menu/.test(w.name);
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.map((window) => window.name);
|
||||
|
||||
menuWindows.forEach((window) => {
|
||||
if (window) {
|
||||
App.get_window(window)?.set_visible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BarLayout, BarLayouts } from 'src/lib/options/options.types';
|
||||
import { Gdk } from 'astal/gtk3';
|
||||
import { range } from 'src/lib/array/helpers';
|
||||
import { BarLayout, BarLayouts } from 'src/lib/options/types';
|
||||
|
||||
/**
|
||||
* Returns the bar layout configuration for a specific monitor
|
||||
@@ -39,3 +41,19 @@ export const isLayoutEmpty = (layout: BarLayout): boolean => {
|
||||
|
||||
return isLeftSectionEmpty && isRightSectionEmpty && isMiddleSectionEmpty;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import options from '../../../options';
|
||||
|
||||
const { showIcon, showTime } = options.bar.clock;
|
||||
|
||||
showIcon.subscribe(() => {
|
||||
if (!showTime.get() && !showIcon.get()) {
|
||||
showTime.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
showTime.subscribe(() => {
|
||||
if (!showTime.get() && !showIcon.get()) {
|
||||
showIcon.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
const { label, icon } = options.bar.windowtitle;
|
||||
|
||||
label.subscribe(() => {
|
||||
if (!label.get() && !icon.get()) {
|
||||
icon.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
icon.subscribe(() => {
|
||||
if (!label.get() && !icon.get()) {
|
||||
label.set(true);
|
||||
}
|
||||
});
|
||||
93
src/components/bar/utils/systemResource/index.ts
Normal file
93
src/components/bar/utils/systemResource/index.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { SizeConverter } from 'src/lib/units/size';
|
||||
import { SizeUnit } from 'src/lib/units/size/types';
|
||||
import { ResourceLabelType, GenericResourceData } from 'src/services/system/types';
|
||||
|
||||
/**
|
||||
* Renders a resource label based on the label type and resource data.
|
||||
*
|
||||
* This function generates a resource label string based on the provided label type, resource data, and rounding option.
|
||||
* It formats the used, total, and free resource values and calculates the percentage if needed.
|
||||
*
|
||||
* @param lblType The type of label to render (used/total, used, free, or percentage).
|
||||
* @param resourceUsage An object containing the resource usage data (used, total, percentage, and free).
|
||||
* @param round A boolean indicating whether to round the values.
|
||||
*
|
||||
* @returns The rendered resource label as a string.
|
||||
*/
|
||||
export const renderResourceLabel = (
|
||||
lblType: ResourceLabelType,
|
||||
resourceUsage: GenericResourceData,
|
||||
round: boolean,
|
||||
unitType?: SizeUnit,
|
||||
): string => {
|
||||
const { used, total, percentage, free } = resourceUsage;
|
||||
const precision = round ? 0 : 2;
|
||||
|
||||
if (lblType === 'used/total') {
|
||||
const totalConverter = SizeConverter.fromBytes(total);
|
||||
const usedConverter = SizeConverter.fromBytes(used);
|
||||
const { unit } = totalConverter.toAuto();
|
||||
const sizeUnit: SizeUnit = unitType ?? unit;
|
||||
|
||||
let usedValue: number;
|
||||
let totalValue: string;
|
||||
|
||||
switch (sizeUnit) {
|
||||
case 'tebibytes':
|
||||
usedValue = usedConverter.toTiB(precision);
|
||||
totalValue = totalConverter.formatTiB(precision);
|
||||
return `${usedValue}/${totalValue}`;
|
||||
case 'gibibytes':
|
||||
usedValue = usedConverter.toGiB(precision);
|
||||
totalValue = totalConverter.formatGiB(precision);
|
||||
return `${usedValue}/${totalValue}`;
|
||||
case 'mebibytes':
|
||||
usedValue = usedConverter.toMiB(precision);
|
||||
totalValue = totalConverter.formatMiB(precision);
|
||||
return `${usedValue}/${totalValue}`;
|
||||
case 'kibibytes':
|
||||
usedValue = usedConverter.toKiB(precision);
|
||||
totalValue = totalConverter.formatKiB(precision);
|
||||
return `${usedValue}/${totalValue}`;
|
||||
default:
|
||||
usedValue = usedConverter.toBytes(precision);
|
||||
totalValue = totalConverter.formatBytes(precision);
|
||||
return `${usedValue}/${totalValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (lblType === 'used') {
|
||||
return SizeConverter.fromBytes(used).formatAuto(precision);
|
||||
}
|
||||
|
||||
if (lblType === 'free') {
|
||||
return SizeConverter.fromBytes(free).formatAuto(precision);
|
||||
}
|
||||
|
||||
return `${percentage}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats a tooltip based on the data type and label type.
|
||||
*
|
||||
* This function generates a tooltip string based on the provided data type and label type.
|
||||
*
|
||||
* @param dataType The type of data to include in the tooltip.
|
||||
* @param lblTyp The type of label to format the tooltip for (used, free, used/total, or percentage).
|
||||
*
|
||||
* @returns The formatted tooltip as a string.
|
||||
*/
|
||||
export const formatTooltip = (dataType: string, lblTyp: ResourceLabelType): string => {
|
||||
switch (lblTyp) {
|
||||
case 'used':
|
||||
return `Used ${dataType}`;
|
||||
case 'free':
|
||||
return `Free ${dataType}`;
|
||||
case 'used/total':
|
||||
return `Used/Total ${dataType}`;
|
||||
case 'percentage':
|
||||
return `Percentage ${dataType} Usage`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { Gtk } from 'astal/gtk3';
|
||||
import { ActiveDevices } from './devices/index.js';
|
||||
import { ActivePlaybacks } from './playbacks/index.js';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { isPrimaryClick } from 'src/lib/utils.js';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
export enum ActiveDeviceMenu {
|
||||
DEVICES = 'devices',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { bind } from 'astal';
|
||||
import { Gdk, Gtk } from 'astal/gtk3';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import { capitalizeFirstLetter, isScrollDown, isScrollUp } from 'src/lib/utils';
|
||||
import options from 'src/options';
|
||||
import options from 'src/configuration';
|
||||
import { isScrollUp, isScrollDown } from 'src/lib/events/mouse';
|
||||
import { capitalizeFirstLetter } from 'src/lib/string/formatters';
|
||||
|
||||
const { raiseMaximumVolume } = options.menus.volume;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { getIcon } from '../../utils';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
export const SliderIcon = ({ type, device }: SliderIconProps): JSX.Element => {
|
||||
const iconBinding = Variable.derive([bind(device, 'volume'), bind(device, 'mute')], (volume, isMuted) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalWp from 'gi://AstalWp?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { bind } from 'astal';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
const DeviceIcon = ({ device, type, icon }: AudioDeviceProps): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { VolumeSliders } from './active/index.js';
|
||||
import options from 'src/options.js';
|
||||
import { bind } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { AvailableDevices } from './available/index.js';
|
||||
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
|
||||
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
|
||||
import options from 'src/configuration';
|
||||
|
||||
export default (): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bind } from 'astal';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
export const ConnectButton = ({ device }: ConnectButtonProps): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { forgetBluetoothDevice } from '../helpers';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
export const ForgetButton = ({ device }: ForgetButtonProps): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bind } from 'astal';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
export const PairButton = ({ device }: PairButtonProps): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bind } from 'astal';
|
||||
import { ActionButton } from './ActionButton';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
export const TrustButton = ({ device }: TrustButtonProps): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import Spinner from 'src/components/shared/Spinner';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { bind } from 'astal';
|
||||
import { DeviceIcon } from './DeviceIcon';
|
||||
import { DeviceName } from './DeviceName';
|
||||
import { DeviceStatus } from './DeviceStatus';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
export const BluetoothDevice = ({ device, connectedDevices }: BluetoothDeviceProps): JSX.Element => {
|
||||
const IsConnectingSpinner = (): JSX.Element => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { isPrimaryClick } from 'src/lib/utils';
|
||||
import { bind, timeout } from 'astal';
|
||||
import { isDiscovering } from './helper';
|
||||
import AstalBluetooth from 'gi://AstalBluetooth?version=0.1';
|
||||
import { isPrimaryClick } from 'src/lib/events/mouse';
|
||||
|
||||
const bluetoothService = AstalBluetooth.get_default();
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import options from 'src/configuration';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { BluetoothDevices } from './devices/index.js';
|
||||
import { Header } from './header/index.js';
|
||||
import options from 'src/options.js';
|
||||
import { bind } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
|
||||
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
|
||||
|
||||
export default (): JSX.Element => {
|
||||
return (
|
||||
|
||||
@@ -2,9 +2,9 @@ import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { TimeWidget } from './time/index';
|
||||
import { CalendarWidget } from './CalendarWidget.js';
|
||||
import { WeatherWidget } from './weather/index';
|
||||
import options from 'src/options';
|
||||
import { bind } from 'astal';
|
||||
import { RevealerTransitionMap } from 'src/lib/constants/options.js';
|
||||
import { RevealerTransitionMap } from 'src/components/settings/constants.js';
|
||||
import options from 'src/configuration';
|
||||
|
||||
const { transition } = options.menus;
|
||||
const { enabled: weatherEnabled } = options.menus.clock.weather;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import options from 'src/options';
|
||||
import { bind, Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { systemTime } from 'src/shared/time';
|
||||
import options from 'src/configuration';
|
||||
import { systemTime } from 'src/lib/units/time';
|
||||
|
||||
const { military, hideSeconds } = options.menus.clock.time;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import options from 'src/options';
|
||||
import { bind, GLib, Variable } from 'astal';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { systemTime } from 'src/shared/time';
|
||||
import options from 'src/configuration';
|
||||
import { systemTime } from 'src/lib/units/time';
|
||||
|
||||
const { military, hideSeconds } = options.menus.clock.time;
|
||||
|
||||
|
||||
@@ -1,64 +1,51 @@
|
||||
import { Weather, WeatherIconTitle } from 'src/lib/types/weather.types';
|
||||
import { isValidWeatherIconTitle } from 'src/shared/weather';
|
||||
import { Weather, WeatherIcon, WeatherStatus } from 'src/services/weather/types';
|
||||
|
||||
/**
|
||||
* Retrieves the next epoch time for weather data.
|
||||
* Calculates the target hour for weather data lookup
|
||||
*
|
||||
* This function calculates the next epoch time based on the current weather data and the specified number of hours from now.
|
||||
* It ensures that the prediction remains within the current day by rewinding the time if necessary.
|
||||
*
|
||||
* @param wthr The current weather data.
|
||||
* @param hoursFromNow The number of hours from now to calculate the next epoch time.
|
||||
*
|
||||
* @returns The next epoch time as a number.
|
||||
* @param baseTime - The base time to calculate from
|
||||
* @param hoursFromNow - Number of hours to add
|
||||
* @returns A Date object set to the start of the target hour
|
||||
*/
|
||||
export const getNextEpoch = (wthr: Weather, hoursFromNow: number): number => {
|
||||
const currentEpoch = wthr.location.localtime_epoch;
|
||||
const epochAtHourStart = currentEpoch - (currentEpoch % 3600);
|
||||
let nextEpoch = 3600 * hoursFromNow + epochAtHourStart;
|
||||
export const getTargetHour = (baseTime: Date, hoursFromNow: number): Date => {
|
||||
const targetTime = new Date(baseTime);
|
||||
const newHour = targetTime.getHours() + hoursFromNow;
|
||||
targetTime.setHours(newHour);
|
||||
targetTime.setMinutes(0, 0, 0);
|
||||
|
||||
const curHour = new Date(currentEpoch * 1000).getHours();
|
||||
|
||||
/*
|
||||
* NOTE: Since the API is only capable of showing the current day; if
|
||||
* the hours left in the day are less than 4 (aka spilling into the next day),
|
||||
* then rewind to contain the prediction within the current day.
|
||||
*/
|
||||
if (curHour > 19) {
|
||||
const hoursToRewind = curHour - 19;
|
||||
nextEpoch = 3600 * hoursFromNow + epochAtHourStart - hoursToRewind * 3600;
|
||||
const currentHour = baseTime.getHours();
|
||||
if (currentHour > 19) {
|
||||
const hoursToRewind = currentHour - 19;
|
||||
targetTime.setHours(targetTime.getHours() - hoursToRewind);
|
||||
}
|
||||
return nextEpoch;
|
||||
|
||||
return targetTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the weather icon query for a specific time in the future.
|
||||
* Retrieves the weather icon for a specific hour in the future
|
||||
*
|
||||
* This function calculates the next epoch time and retrieves the corresponding weather data.
|
||||
* It then generates a weather icon query based on the weather condition and time of day.
|
||||
*
|
||||
* @param weather The current weather data.
|
||||
* @param hoursFromNow The number of hours from now to calculate the weather icon query.
|
||||
*
|
||||
* @returns The weather icon query as a string.
|
||||
* @param weather - The current weather data
|
||||
* @param hoursFromNow - Number of hours from now to get the icon for
|
||||
* @returns The appropriate weather icon
|
||||
*/
|
||||
export const getIconQuery = (weather: Weather, hoursFromNow: number): WeatherIconTitle => {
|
||||
const nextEpoch = getNextEpoch(weather, hoursFromNow);
|
||||
const weatherAtEpoch = weather.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
|
||||
|
||||
if (weatherAtEpoch === undefined) {
|
||||
return 'warning';
|
||||
export const getHourlyWeatherIcon = (weather: Weather, hoursFromNow: number): WeatherIcon => {
|
||||
if (!weather?.forecast?.[0]?.hourly) {
|
||||
return WeatherIcon.WARNING;
|
||||
}
|
||||
|
||||
let iconQuery = weatherAtEpoch.condition.text.trim().toLowerCase().replaceAll(' ', '_');
|
||||
const targetHour = getTargetHour(weather.lastUpdated, hoursFromNow);
|
||||
const targetTime = targetHour.getTime();
|
||||
|
||||
if (!weatherAtEpoch?.is_day && iconQuery === 'partly_cloudy') {
|
||||
iconQuery = 'partly_cloudy_night';
|
||||
const weatherAtHour = weather.forecast[0].hourly.find((hour) => {
|
||||
const hourTime = hour.time.getTime();
|
||||
return hourTime === targetTime;
|
||||
});
|
||||
|
||||
if (!weatherAtHour) {
|
||||
return WeatherIcon.WARNING;
|
||||
}
|
||||
|
||||
if (isValidWeatherIconTitle(iconQuery)) {
|
||||
return iconQuery;
|
||||
} else {
|
||||
return 'warning';
|
||||
}
|
||||
const iconQuery: WeatherStatus = weatherAtHour.condition?.text ?? 'WARNING';
|
||||
return WeatherIcon[iconQuery];
|
||||
};
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { bind } from 'astal';
|
||||
import { globalWeatherVar } from 'src/shared/weather';
|
||||
import { Gtk } from 'astal/gtk3';
|
||||
import { weatherIcons } from 'src/lib/icons/weather.js';
|
||||
import { getIconQuery } from '../helpers';
|
||||
import WeatherService from 'src/services/weather';
|
||||
import { getHourlyWeatherIcon } from '../helpers';
|
||||
|
||||
const weatherService = WeatherService.getInstance();
|
||||
|
||||
export const HourlyIcon = ({ hoursFromNow }: HourlyIconProps): JSX.Element => {
|
||||
return (
|
||||
<box halign={Gtk.Align.CENTER}>
|
||||
<label
|
||||
className={'hourly-weather-icon txt-icon'}
|
||||
label={bind(globalWeatherVar).as((weather) => {
|
||||
const iconQuery = getIconQuery(weather, hoursFromNow);
|
||||
const weatherIcn = weatherIcons[iconQuery] || weatherIcons['warning'];
|
||||
return weatherIcn;
|
||||
label={bind(weatherService.weatherData).as((weather) => {
|
||||
const weatherIcon = getHourlyWeatherIcon(weather, hoursFromNow);
|
||||
return weatherIcon;
|
||||
})}
|
||||
halign={Gtk.Align.CENTER}
|
||||
/>
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
import options from 'src/options';
|
||||
import { globalWeatherVar } from 'src/shared/weather';
|
||||
import { getNextEpoch } from '../helpers';
|
||||
import { bind, Variable } from 'astal';
|
||||
import options from 'src/configuration';
|
||||
import WeatherService from 'src/services/weather';
|
||||
import { getTargetHour } from '../helpers';
|
||||
import { TemperatureConverter } from 'src/lib/units/temperature';
|
||||
|
||||
const weatherService = WeatherService.getInstance();
|
||||
|
||||
const { unit } = options.menus.clock.weather;
|
||||
|
||||
export const HourlyTemp = ({ hoursFromNow }: HourlyTempProps): JSX.Element => {
|
||||
const weatherBinding = Variable.derive([bind(globalWeatherVar), bind(unit)], (weather, unitType) => {
|
||||
if (!Object.keys(weather).length) {
|
||||
return '-';
|
||||
}
|
||||
const weatherBinding = Variable.derive(
|
||||
[bind(weatherService.weatherData), bind(unit)],
|
||||
(weather, unitType) => {
|
||||
if (!Object.keys(weather).length || !weather?.forecast?.[0]?.hourly) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const nextEpoch = getNextEpoch(weather, hoursFromNow);
|
||||
const weatherAtEpoch = weather.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
|
||||
const targetHour = getTargetHour(new Date(), hoursFromNow);
|
||||
const weatherAtTargetHour = weather.forecast[0].hourly.find(
|
||||
(h) => h.time.getTime() === targetHour.getTime(),
|
||||
);
|
||||
const temperatureAtTargetHour = weatherAtTargetHour?.temperature ?? 0;
|
||||
|
||||
if (unitType === 'imperial') {
|
||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : '-'}° F`;
|
||||
}
|
||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : '-'}° C`;
|
||||
});
|
||||
const tempConverter = TemperatureConverter.fromCelsius(temperatureAtTargetHour);
|
||||
const isImperial = unitType === 'imperial';
|
||||
|
||||
return isImperial ? tempConverter.formatFahrenheit() : tempConverter.formatCelsius();
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<label
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user