Upgrade to Agsv2 + Astal (#533)

* migrate to astal

* Reorganize project structure.

* progress

* Migrate Dashboard and Window Title modules.

* Migrate clock and notification bar modules.

* Remove unused code

* Media menu

* Rework network and volume modules

* Finish custom modules.

* Migrate battery bar module.

* Update battery module and organize helpers.

* Migrate workspace module.

* Wrap up bar modules.

* Checkpoint before I inevitbly blow something up.

* Updates

* Fix event propagation logic.

* Type fixes

* More type fixes

* Fix padding for event boxes.

* Migrate volume menu and refactor scroll event handlers.

* network module WIP

* Migrate network service.

* Migrate bluetooth menu

* Updates

* Migrate notifications

* Update scrolling behavior for custom modules.

* Improve popup notifications and add timer functionality.

* Migration notifications menu header/controls.

* Migrate notifications menu and consolidate notifications menu code.

* Migrate power menu.

* Dashboard progress

* Migrate dashboard

* Migrate media menu.

* Reduce media menu nesting.

* Finish updating media menu bindings to navigate active player.

* Migrate battery menu

* Consolidate code

* Migrate calendar menu

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

* Migrate osd

* Consolidate hyprland service connections.

* Implement startup dropdown menu position allocation.

* Migrate settings menu (WIP)

* Settings dialo menu fixes

* Finish Dashboard menu

* Type updates

* update submoldule for types

* update github ci

* ci

* Submodule update

* Ci updates

* Remove type checking for now.

* ci fix

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

* Validate dropdown menu before render.

* Consolidate code and add auto-hide functionality.

* Improve auto-hide behavior.

* Consolidate audio menu code

* Organize bluetooth code

* Improve active player logic

* Properly dismiss a notification on action button resolution.

* Implement CLI command engine and migrate CLI commands.

* Handle variable disposal

* Bar component fixes and add hyprland startup rules.

* Handle potentially null bindings network and bluetooth bindings.

* Handle potentially null wired adapter.

* Fix GPU stats

* Handle poller for GPU

* Fix gpu bar logic.

* Clean up logic for stat bars.

* Handle wifi and wired bar icon bindings.

* Fix battery percentages

* Fix switch behavior

* Wifi staging fixes

* Reduce redundant hyprland service calls.

* Code cleanup

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

* Remove outdated comment.

* Add JSDocs

* Add meson to build hyprpanel

* Consistency updates

* Organize commands

* Fix images not showing up on notifications.

* Remove todo

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

* Handle SRC directory for the bundled/built hyprpanel.

* Add namespaces to all windows

* Migrate systray

* systray updates

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

* Remove log from meson

* Fix file choose path and make it float.

* Added a command to check the dependency status

* Update dep names.

* Get scale directly from env

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

View File

@@ -8,7 +8,7 @@ module.exports = {
plugins: ['@typescript-eslint', 'import'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
root: true,
ignorePatterns: ['.eslintrc.js', 'types/**/*.ts', 'scripts/**/*.js'],
ignorePatterns: ["/*", "!/src"],
env: {
es6: true,
browser: true,

View File

@@ -10,21 +10,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout main repository
- name: Checkout main repository with submodules
uses: actions/checkout@v3
- name: Clone ags-types to temp dir
uses: actions/checkout@v3
with:
repository: Jas-SinghFSU/ags-types
path: temp-ags-types
- name: Copy types to types/
run: |
rm -rf types
mkdir -p types
cp -R temp-ags-types/types/* types/
rm -rf temp-ags-types
# with:
# submodules: true
#
# - name: Clone astal repository to /usr/share/astal/gjs
# run: |
# sudo mkdir -p /usr/share/astal/
# sudo git clone https://github.com/Jas-SinghFSU/astalgjs.git /usr/share/astal
#
# - name: Copy types to @girs/
# run: |
# rm -rf @girs
# mkdir -p @girs
# cp -R external/ags-types/@girs/* @girs/
# rm -rf external/ags-types
- name: Node Setup
uses: actions/setup-node@v3
@@ -37,5 +38,5 @@ jobs:
- name: ESLint
run: npm run lint
- name: Type Check
run: npx tsc --noEmit --pretty --extendedDiagnostics
# - name: Type Check
# run: npx tsc --noEmit --pretty --extendedDiagnostics

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
.weather.json
node_modules
@girs

5
.gitmodules vendored
View File

@@ -1,3 +1,4 @@
[submodule "external/ags-types"]
path = external/ags-types
url = https://github.com/Jas-SinghFSU/ags-types.git
path = external/ags-types
url = https://github.com/Jas-SinghFSU/ags-types.git

View File

@@ -1,49 +0,0 @@
# Big thanks to kotontrio for providing this. You can find it in his repo as well
# as: https://github.com/kotontrion/PKGBUILDS/blob/main/agsv1/PKGBUILD
#
# Maintainer: kotontrion <kotontrion@tutanota.de>
# This package is only intended to be used while migrating from ags v1.8.2 to ags v2.0.0.
# Many ags configs are quite big and it takes a while to migrate, therefore I made this package
# to install ags v1.8.2 as "agsv1", so both versions can be installed at the same time, making it
# possible to migrate bit by bit while still having a working v1 config around.
#
# First update the aylurs-gtk-shell package to v2, then install this one.
#
# This package won't receive any updates anymore, so as soon as you migrated, uninstall this one.
pkgname=agsv1
_pkgname=ags
pkgver=1.8.2
pkgrel=1
pkgdesc="Aylurs's Gtk Shell (AGS), An eww inspired gtk widget system."
arch=('x86_64')
url="https://github.com/Aylur/ags"
license=('GPL-3.0-only')
makedepends=('gobject-introspection' 'meson' 'glib2-devel' 'npm' 'typescript')
depends=('gjs' 'glib2' 'glibc' 'gtk3' 'gtk-layer-shell' 'libpulse' 'pam')
optdepends=('gnome-bluetooth-3.0: required for bluetooth service'
'greetd: required for greetd service'
'libdbusmenu-gtk3: required for systemtray service'
'libsoup3: required for the Utils.fetch feature'
'libnotify: required for sending notifications'
'networkmanager: required for network service'
'power-profiles-daemon: required for powerprofiles service'
'upower: required for battery service')
backup=('etc/pam.d/ags')
source=("$pkgname-$pkgver.tar.gz::https://github.com/Aylur/ags/releases/download/v${pkgver}/ags-v${pkgver}.tar.gz")
sha256sums=('ea0a706bef99578b30d40a2d0474b7a251364bfcf3a18cdc9b1adbc04af54773')
build() {
cd $srcdir/$_pkgname
npm install
arch-meson build --libdir "lib/$_pkgname" -Dbuild_types=true
meson compile -C build
}
package() {
cd $srcdir/$_pkgname
meson install -C build --destdir "$pkgdir"
rm ${pkgdir}/usr/bin/ags
ln -sf /usr/share/com.github.Aylur.ags/com.github.Aylur.ags ${pkgdir}/usr/bin/agsv1
}

107
app.ts Normal file
View File

@@ -0,0 +1,107 @@
import './src/lib/session';
import './src/scss/style';
import './src/globals/useTheme';
import './src/globals/wallpaper';
import './src/globals/systray';
import './src/globals/dropdown';
import './src/globals/utilities';
import './src/components/bar/utils/sideEffects';
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 { exec, execAsync } from 'astal';
import { hyprlandService } from 'src/lib/constants/services';
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';
const initializeStartupScripts = (): void => {
execAsync(`python3 ${SRC_DIR}/scripts/bluetooth.py`).catch((err) => console.error(err));
};
const initializeMenus = (): void => {
StandardWindows.forEach((window) => {
return window();
});
DropdownMenus.forEach((window) => {
return window();
});
DropdownMenus.forEach((window) => {
const windowName = window.name.replace('_default', '').concat('menu').toLowerCase();
if (!isDropdownMenu(windowName)) {
return;
}
handleRealization(windowName);
});
};
App.start({
instanceName: 'hyprpanel',
requestHandler(request: string, res: (response: unknown) => void) {
runCLI(request, res);
},
main() {
initializeStartupScripts();
Notifications();
OSD();
forMonitors(Bar).forEach((bar: JSX.Element) => bar);
SettingsDialog();
initializeMenus();
initializeSystemBehaviors();
},
});
/**
* Function to determine if the current OS is NixOS by parsing /etc/os-release.
* @returns True if NixOS, false otherwise.
*/
const isNixOS = (): boolean => {
try {
const osRelease = exec('cat /etc/os-release').toString();
const idMatch = osRelease.match(/^ID\s*=\s*"?([^"\n]+)"?/m);
if (idMatch && idMatch[1].toLowerCase() === 'nixos') {
return true;
}
return false;
} catch (error) {
console.error('Error detecting OS:', error);
return false;
}
};
/**
* Function to generate the appropriate restart command based on the OS.
* @returns The modified or original restart command.
*/
const getRestartCommand = (): string => {
const isNix = isNixOS();
const command = options.hyprpanel.restartCommand.get();
if (isNix) {
return command.replace(/\bags\b/g, 'hyprpanel');
}
return command;
};
hyprlandService.connect('monitor-added', () => {
if (options.hyprpanel.restartAgs.get()) {
const restartAgsCommand = getRestartCommand();
bash(restartAgsCommand);
}
});

1
astal Symbolic link
View File

@@ -0,0 +1 @@
/usr/share/astal/gjs

View File

@@ -1,58 +0,0 @@
import GLib from 'gi://GLib';
const main = '/tmp/ags/hyprpanel/main.js';
const entry = `${App.configDir}/main.ts`;
const bundler = GLib.getenv('AGS_BUNDLER') || 'bun';
const v = {
ags: pkg.version?.split('.').map(Number) || [],
expect: [1, 8, 1],
};
try {
switch (bundler) {
case 'bun':
await Utils.execAsync([
'bun',
'build',
entry,
'--outfile',
main,
'--external',
'resource://*',
'--external',
'gi://*',
'--external',
'file://*',
]);
break;
case 'esbuild':
await Utils.execAsync([
'esbuild',
'--bundle',
entry,
'--format=esm',
`--outfile=${main}`,
'--external:resource://*',
'--external:gi://*',
'--external:file://*',
]);
break;
default:
throw `"${bundler}" is not a valid bundler`;
}
if (v.ags[1] < v.expect[1] || v.ags[2] < v.expect[2]) {
print(`HyprPanel needs atleast v${v.expect.join('.')} of AGS, yours is v${v.ags.join('.')}`);
App.quit();
}
await import(`file://${main}`);
} catch (error) {
console.error(error);
App.quit();
}
export {};

View File

@@ -1,830 +0,0 @@
import { Option } from 'widget/settings/shared/Option';
import { Header } from 'widget/settings/shared/Header';
import options from 'options';
import Scrollable from 'types/widgets/scrollable';
import { Attribute, GtkWidget } from 'lib/types/widget';
export const CustomModuleSettings = (): Scrollable<GtkWidget, Attribute> =>
Widget.Scrollable({
vscroll: 'automatic',
hscroll: 'automatic',
class_name: 'menu-theme-page customModules paged-container',
child: Widget.Box({
class_name: 'menu-theme-page paged-container',
vertical: true,
children: [
/*
************************************
* GENERAL *
************************************
*/
Header('General'),
Option({
opt: options.bar.customModules.scrollSpeed,
title: 'Scrolling Speed',
type: 'number',
}),
/*
************************************
* RAM *
************************************
*/
Header('RAM'),
Option({
opt: options.theme.bar.buttons.modules.ram.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.ram.icon,
title: 'Ram Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.ram.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.labelType,
title: 'Label Type',
type: 'enum',
enums: ['used/total', 'used', 'free', 'percentage'],
}),
Option({
opt: options.bar.customModules.ram.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.ram.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.ram.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.ram.middleClick,
title: 'Middle Click',
type: 'string',
}),
/*
************************************
* CPU *
************************************
*/
Header('CPU'),
Option({
opt: options.theme.bar.buttons.modules.cpu.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.cpu.icon,
title: 'Cpu Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.cpu.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.cpu.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.cpu.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpu.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* CPU TEMP *
************************************
*/
Header('CPU Temperature'),
Option({
opt: options.theme.bar.buttons.modules.cpuTemp.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.cpuTemp.sensor,
title: 'CPU Temperature Sensor',
subtitle: 'Wiki: https://hyprpanel.com/configuration/panel.html#custom-modules',
subtitleLink: 'https://hyprpanel.com/configuration/panel.html#custom-modules',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpuTemp.unit,
title: 'CPU Temperature Unit',
type: 'enum',
enums: ['imperial', 'metric'],
}),
Option({
opt: options.bar.customModules.cpuTemp.showUnit,
title: 'Show Unit',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.cpuTemp.icon,
title: 'Cpu Temperature Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpuTemp.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.cpuTemp.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpuTemp.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.cpuTemp.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.cpuTemp.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpuTemp.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpuTemp.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpuTemp.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.cpuTemp.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* STORAGE *
************************************
*/
Header('Storage'),
Option({
opt: options.theme.bar.buttons.modules.storage.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.storage.icon,
title: 'Storage Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.storage.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.labelType,
title: 'Label Type',
type: 'enum',
enums: ['used/total', 'used', 'free', 'percentage'],
}),
Option({
opt: options.bar.customModules.storage.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.storage.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.storage.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.storage.middleClick,
title: 'Middle Click',
type: 'string',
}),
/*
************************************
* NETSTAT *
************************************
*/
Header('Netstat'),
Option({
opt: options.theme.bar.buttons.modules.netstat.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.netstat.networkInterface,
title: 'Network Interface',
subtitle:
"Name of the network interface to poll.\nHINT: Get list of interfaces with 'cat /proc/net/dev'",
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.dynamicIcon,
title: 'Use Network Icon',
subtitle: 'If enabled, shows the current network icon indicators instead of the static icon',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.netstat.icon,
title: 'Netstat Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.netstat.rateUnit,
title: 'Rate Unit',
type: 'enum',
enums: ['GiB', 'MiB', 'KiB', 'auto'],
}),
Option({
opt: options.theme.bar.buttons.modules.netstat.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.labelType,
title: 'Label Type',
type: 'enum',
enums: ['full', 'in', 'out'],
}),
Option({
opt: options.bar.customModules.netstat.round,
title: 'Round',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.netstat.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.netstat.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.netstat.middleClick,
title: 'Middle Click',
type: 'string',
}),
/*
************************************
* KEYBOARD LAYOUT *
************************************
*/
Header('Keyboard Layout'),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.kbLayout.icon,
title: 'Keyboard Layout Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.kbLayout.labelType,
title: 'Label Type',
type: 'enum',
enums: ['layout', 'code'],
}),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.kbLayout.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* UPDATES *
************************************
*/
Header('Updates'),
Option({
opt: options.theme.bar.buttons.modules.updates.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.updates.updateCommand,
title: 'Check Updates Command',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.icon,
title: 'Updates Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.updates.padZero,
title: 'Pad with 0',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.updates.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.pollingInterval,
title: 'Polling Interval',
type: 'number',
subtitle: "WARNING: Be careful of your package manager's rate limit.",
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.updates.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.updates.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* SUBMAP *
************************************
*/
Header('Submap'),
Option({
opt: options.theme.bar.buttons.modules.submap.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.submap.showSubmapName,
title: 'Show Submap Name',
subtitle:
'When enabled, the name of the current submap will be displayed' +
' instead of the Submap Enabled or Disabled text.',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.submap.enabledIcon,
title: 'Enabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.disabledIcon,
title: 'Disabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.enabledText,
title: 'Enabled Text',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.disabledText,
title: 'Disabled Text',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.submap.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.submap.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* WEATHER *
************************************
*/
Header('Weather'),
Option({
opt: options.theme.bar.buttons.modules.weather.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.weather.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.weather.unit,
title: 'Units',
type: 'enum',
enums: ['imperial', 'metric'],
}),
Option({
opt: options.theme.bar.buttons.modules.weather.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.weather.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* HYPRSUNSET *
************************************
*/
Header('Hyprsunset'),
Option({
opt: options.bar.customModules.hyprsunset.temperature,
title: 'Temperature',
subtitle: 'Ex: 1000k, 2000k, 5000k, etc.',
type: 'string',
}),
Option({
opt: options.theme.bar.buttons.modules.hyprsunset.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.hyprsunset.onIcon,
title: 'Enabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.offIcon,
title: 'Disabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.onLabel,
title: 'Enabled Label',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.offLabel,
title: 'Disabled Label',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.hyprsunset.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.hyprsunset.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.hyprsunset.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* HYPRIDLE *
************************************
*/
Header('Hypridle'),
Option({
opt: options.theme.bar.buttons.modules.hypridle.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.bar.customModules.hypridle.onIcon,
title: 'Enabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.offIcon,
title: 'Disabled Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.onLabel,
title: 'Enabled Label',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.offLabel,
title: 'Disabled Label',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.label,
title: 'Show Label',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.hypridle.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.pollingInterval,
title: 'Polling Interval',
type: 'number',
min: 100,
max: 60 * 24 * 1000,
increment: 1000,
}),
Option({
opt: options.bar.customModules.hypridle.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.hypridle.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
/*
************************************
* POWER *
************************************
*/
Header('Power'),
Option({
opt: options.theme.bar.buttons.modules.power.enableBorder,
title: 'Button Border',
type: 'boolean',
}),
Option({
opt: options.theme.bar.buttons.modules.power.spacing,
title: 'Spacing',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.icon,
title: 'Power Button Icon',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.leftClick,
title: 'Left Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.rightClick,
title: 'Right Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.middleClick,
title: 'Middle Click',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.scrollUp,
title: 'Scroll Up',
type: 'string',
}),
Option({
opt: options.bar.customModules.power.scrollDown,
title: 'Scroll Down',
type: 'string',
}),
],
}),
});

View File

@@ -1,70 +0,0 @@
import { module } from '../module';
import options from 'options';
import Button from 'types/widgets/button';
// Utility Methods
import { inputHandler } from 'customModules/utils';
import { computeCPU } from './computeCPU';
import { BarBoxChild } from 'lib/types/bar';
import { Attribute, Child } from 'lib/types/widget';
import { FunctionPoller } from 'lib/poller/FunctionPoller';
// All the user configurable options for the cpu module that are needed
const { label, round, leftClick, rightClick, middleClick, scrollUp, scrollDown, pollingInterval, icon } =
options.bar.customModules.cpu;
export const cpuUsage = Variable(0);
// Instantiate the Poller class for CPU usage polling
const cpuPoller = new FunctionPoller<number, []>(
// Variable to poll and update with the result of the function passed in
cpuUsage,
// Variables that should trigger the polling function to update when they change
[round.bind('value')],
// Interval at which to poll
pollingInterval.bind('value'),
// Function to execute to get the network data
computeCPU,
);
cpuPoller.initialize('cpu');
export const Cpu = (): BarBoxChild => {
const renderLabel = (cpuUsg: number, rnd: boolean): string => {
return rnd ? `${Math.round(cpuUsg)}%` : `${cpuUsg.toFixed(2)}%`;
};
const cpuModule = module({
textIcon: icon.bind('value'),
label: Utils.merge([cpuUsage.bind('value'), round.bind('value')], (cpuUsg, rnd) => {
return renderLabel(cpuUsg, rnd);
}),
tooltipText: 'CPU',
boxClass: 'cpu',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button<Child, Attribute>) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return cpuModule;
};

View File

@@ -1,37 +0,0 @@
import GLib from 'gi://GLib?version=2.0';
import { convertCelsiusToFahrenheit } from 'globals/weather';
import { UnitType } from 'lib/types/weather';
import options from 'options';
import { Variable as VariableType } from 'types/variable';
const { sensor } = options.bar.customModules.cpuTemp;
/**
* Retrieves the current CPU temperature.
* @returns CPU temperature in degrees Celsius
*/
export const getCPUTemperature = (round: VariableType<boolean>, unit: VariableType<UnitType>): number => {
try {
if (sensor.value.length === 0) {
return 0;
}
const [success, tempInfoBytes] = GLib.file_get_contents(sensor.value);
const tempInfo = new TextDecoder('utf-8').decode(tempInfoBytes);
if (!success || !tempInfoBytes) {
console.error(`Failed to read ${sensor.value} or file content is null.`);
return 0;
}
let decimalTemp = parseInt(tempInfo, 10) / 1000;
if (unit.value === 'imperial') {
decimalTemp = convertCelsiusToFahrenheit(decimalTemp);
}
return round.value ? Math.round(decimalTemp) : parseFloat(decimalTemp.toFixed(2));
} catch (error) {
console.error('Error calculating CPU Temp:', error);
return 0;
}
};

View File

@@ -1,89 +0,0 @@
import options from 'options';
// Module initializer
import { module } from '../module';
import Button from 'types/widgets/button';
// Utility Methods
import { inputHandler } from 'customModules/utils';
import { getCPUTemperature } from './helpers';
import { BarBoxChild } from 'lib/types/bar';
import { Attribute, Child } from 'lib/types/widget';
import { FunctionPoller } from 'lib/poller/FunctionPoller';
import { Variable as VariableType } from 'types/variable';
import { UnitType } from 'lib/types/weather';
// All the user configurable options for the cpu module that are needed
const {
label,
sensor,
round,
showUnit,
unit,
leftClick,
rightClick,
middleClick,
scrollUp,
scrollDown,
pollingInterval,
icon,
} = options.bar.customModules.cpuTemp;
export const cpuTemp = Variable(0);
const cpuTempPoller = new FunctionPoller<number, [VariableType<boolean>, VariableType<UnitType>]>(
// Variable to poll and update with the result of the function passed in
cpuTemp,
// Variables that should trigger the polling function to update when they change
[sensor.bind('value'), round.bind('value'), unit.bind('value')],
// Interval at which to poll
pollingInterval.bind('value'),
// Function to execute to get the network data
getCPUTemperature,
round,
unit,
);
cpuTempPoller.initialize('cputemp');
export const CpuTemp = (): BarBoxChild => {
const cpuTempModule = module({
textIcon: icon.bind('value'),
label: Utils.merge(
[cpuTemp.bind('value'), unit.bind('value'), showUnit.bind('value'), round.bind('value')],
(cpuTmp, tempUnit, shwUnit) => {
const unitLabel = tempUnit === 'imperial' ? 'F' : 'C';
const unit = shwUnit ? ` ${unitLabel}` : '';
return `${cpuTmp.toString()}°${unit}`;
},
),
tooltipText: 'CPU Temperature',
boxClass: 'cpu-temp',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button<Child, Attribute>) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return cpuTempModule;
};

View File

@@ -1,29 +0,0 @@
import { Variable as TVariable } from 'types/variable';
export const isActiveCommand = `bash -c "pgrep -x 'hypridle' &>/dev/null && echo 'yes' || echo 'no'"`;
export const isActive = Variable(false);
export const toggleIdle = (isActive: TVariable<boolean>): void => {
Utils.execAsync(isActiveCommand).then((res) => {
if (res === 'no') {
Utils.execAsync(`bash -c "nohup hypridle > /dev/null 2>&1 &"`).then(() => {
Utils.execAsync(isActiveCommand).then((res) => {
isActive.value = res === 'yes';
});
});
} else {
Utils.execAsync(`bash -c "pkill hypridle "`).then(() => {
Utils.execAsync(isActiveCommand).then((res) => {
isActive.value = res === 'yes';
});
});
}
});
};
export const checkIdleStatus = (): undefined => {
Utils.execAsync(isActiveCommand).then((res) => {
isActive.value = res === 'yes';
});
};

View File

@@ -1,33 +0,0 @@
import options from 'options';
import { Variable as TVariable } from 'types/variable';
const { temperature } = options.bar.customModules.hyprsunset;
export const isActiveCommand = `bash -c "pgrep -x 'hyprsunset' > /dev/null && echo 'yes' || echo 'no'"`;
export const isActive = Variable(false);
export const toggleSunset = (isActive: TVariable<boolean>): void => {
Utils.execAsync(isActiveCommand).then((res) => {
if (res === 'no') {
Utils.execAsync(`bash -c "nohup hyprsunset -t ${temperature.value} > /dev/null 2>&1 &"`).then(() => {
Utils.execAsync(isActiveCommand).then((res) => {
isActive.value = res === 'yes';
});
});
} else {
Utils.execAsync(`bash -c "pkill hyprsunset "`).then(() => {
Utils.execAsync(isActiveCommand).then((res) => {
isActive.value = res === 'yes';
});
});
}
});
};
export const checkSunsetStatus = (): undefined => {
Utils.execAsync(isActiveCommand).then((res) => {
isActive.value = res === 'yes';
});
};

View File

@@ -1,87 +0,0 @@
import { BarBoxChild, Module } from 'lib/types/bar';
import { BarButtonStyles } from 'lib/types/options';
import { GtkWidget } from 'lib/types/widget';
import options from 'options';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0';
const { style } = options.theme.bar.buttons;
const undefinedVar = Variable(undefined);
export const module = ({
icon,
textIcon,
useTextIcon = Variable(false).bind('value'),
label,
tooltipText,
boxClass,
props = {},
showLabelBinding = undefinedVar.bind('value'),
showLabel,
labelHook,
hook,
}: Module): BarBoxChild => {
const getIconWidget = (useTxtIcn: boolean): GtkWidget | undefined => {
let iconWidget: Gtk.Widget | undefined;
if (icon !== undefined && !useTxtIcn) {
iconWidget = Widget.Icon({
class_name: `txt-icon bar-button-icon module-icon ${boxClass}`,
icon: icon,
});
} else if (textIcon !== undefined) {
iconWidget = Widget.Label({
class_name: `txt-icon bar-button-icon module-icon ${boxClass}`,
label: textIcon,
});
}
return iconWidget;
};
return {
component: Widget.Box({
className: Utils.merge(
[style.bind('value'), showLabelBinding],
(style: BarButtonStyles, shwLabel: boolean) => {
const shouldShowLabel = shwLabel || showLabel;
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `${boxClass} ${styleMap[style]} ${!shouldShowLabel ? 'no-label' : ''}`;
},
),
tooltip_text: tooltipText,
children: Utils.merge(
[showLabelBinding, useTextIcon],
(showLabel: boolean, forceTextIcon: boolean): Gtk.Widget[] => {
const childrenArray: Gtk.Widget[] = [];
const iconWidget = getIconWidget(forceTextIcon);
if (iconWidget !== undefined) {
childrenArray.push(iconWidget);
}
if (showLabel) {
childrenArray.push(
Widget.Label({
class_name: `bar-button-label module-label ${boxClass}`,
label: label,
setup: labelHook,
}),
);
}
return childrenArray;
},
),
setup: hook,
}),
tooltip_text: tooltipText,
isVisible: true,
boxClass,
props,
};
};

View File

@@ -1,120 +0,0 @@
const network = await Service.import('network');
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import { computeNetwork } from './computeNetwork';
import { BarBoxChild, NetstatLabelType, RateUnit } from 'lib/types/bar';
import Button from 'types/widgets/button';
import { NetworkResourceData } from 'lib/types/customModules/network';
import { NETWORK_LABEL_TYPES } from 'lib/types/defaults/bar';
import { GET_DEFAULT_NETSTAT_DATA } from 'lib/types/defaults/netstat';
import { Attribute, Child } from 'lib/types/widget';
import { FunctionPoller } from 'lib/poller/FunctionPoller';
import { Variable as TVariable } from 'types/variable';
const {
label,
labelType,
networkInterface,
rateUnit,
dynamicIcon,
icon,
round,
leftClick,
rightClick,
middleClick,
pollingInterval,
} = options.bar.customModules.netstat;
export const networkUsage = Variable<NetworkResourceData>(GET_DEFAULT_NETSTAT_DATA(rateUnit.value));
const netstatPoller = new FunctionPoller<
NetworkResourceData,
[round: TVariable<boolean>, interfaceNameVar: TVariable<string>, dataType: TVariable<RateUnit>]
>(
// Variable to poll and update with the result of the function passed in
networkUsage,
// Variables that should trigger the polling function to update when they change
[rateUnit.bind('value'), networkInterface.bind('value'), round.bind('value')],
// Interval at which to poll
pollingInterval.bind('value'),
// Function to execute to get the network data
computeNetwork,
// Optional parameters to pass to the function
// round is a boolean that determines whether to round the values
round,
// Optional parameters to pass to the function
// networkInterface is the interface name to filter the data
networkInterface,
// Optional parameters to pass to the function
// rateUnit is the unit to display the data in
// e.g. KiB, MiB, GiB, etc.
rateUnit,
);
netstatPoller.initialize('netstat');
export const Netstat = (): BarBoxChild => {
const renderNetworkLabel = (lblType: NetstatLabelType, network: NetworkResourceData): string => {
switch (lblType) {
case 'in':
return `${network.in}`;
case 'out':
return `${network.out}`;
default:
return `${network.in}${network.out}`;
}
};
const netstatModule = module({
useTextIcon: dynamicIcon.bind('value').as((useDynamicIcon) => !useDynamicIcon),
icon: Utils.merge([network.bind('primary'), network.bind('wifi'), network.bind('wired')], (pmry, wfi, wrd) => {
if (pmry === 'wired') {
return wrd.icon_name;
}
return wfi.icon_name;
}),
textIcon: icon.bind('value'),
label: Utils.merge(
[networkUsage.bind('value'), labelType.bind('value')],
(network: NetworkResourceData, lblTyp: NetstatLabelType) => renderNetworkLabel(lblTyp, network),
),
tooltipText: labelType.bind('value').as((lblTyp) => {
return lblTyp === 'full' ? 'Ingress / Egress' : lblTyp === 'in' ? 'Ingress' : 'Egress';
}),
boxClass: 'netstat',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button<Child, Attribute>) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
fn: () => {
labelType.value = NETWORK_LABEL_TYPES[
(NETWORK_LABEL_TYPES.indexOf(labelType.value) + 1) % NETWORK_LABEL_TYPES.length
] as NetstatLabelType;
},
},
onScrollDown: {
fn: () => {
labelType.value = NETWORK_LABEL_TYPES[
(NETWORK_LABEL_TYPES.indexOf(labelType.value) - 1 + NETWORK_LABEL_TYPES.length) %
NETWORK_LABEL_TYPES.length
] as NetstatLabelType;
},
},
});
},
},
});
return netstatModule;
};

View File

@@ -1,88 +0,0 @@
import options from 'options';
// Module initializer
import { module } from '../module';
// Types
import { GenericResourceData } from 'lib/types/customModules/generic';
import Button from 'types/widgets/button';
// Helper Methods
import { calculateRamUsage } from './computeRam';
// Utility Methods
import { formatTooltip, inputHandler, renderResourceLabel } from 'customModules/utils';
import { BarBoxChild, ResourceLabelType } from 'lib/types/bar';
// Global Constants
import { LABEL_TYPES } from 'lib/types/defaults/bar';
import { Attribute, Child } from 'lib/types/widget';
import { FunctionPoller } from 'lib/poller/FunctionPoller';
import { Variable as TVariable } from 'types/variable';
// All the user configurable options for the ram module that are needed
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, [TVariable<boolean>]>(
ramUsage,
[round.bind('value')],
pollingInterval.bind('value'),
calculateRamUsage,
round,
);
ramPoller.initialize('ram');
export const Ram = (): BarBoxChild => {
const ramModule = module({
textIcon: icon.bind('value'),
label: Utils.merge(
[ramUsage.bind('value'), labelType.bind('value'), round.bind('value')],
(rmUsg: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
const returnValue = renderResourceLabel(lblTyp, rmUsg, round);
return returnValue;
},
),
tooltipText: labelType.bind('value').as((lblTyp) => {
return formatTooltip('RAM', lblTyp);
}),
boxClass: 'ram',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button<Child, Attribute>) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) + 1) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
onScrollDown: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
});
},
},
});
return ramModule;
};

View File

@@ -1,29 +0,0 @@
// @ts-expect-error is a special directive that tells the compiler to use the GTop library
import GTop from 'gi://GTop';
import { divide } from 'customModules/utils';
import { Variable as VariableType } from 'types/variable';
import { GenericResourceData } from 'lib/types/customModules/generic';
// FIX: Consolidate with Storage service class
export const computeStorage = (round: VariableType<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.value),
};
} catch (error) {
console.error('Error calculating RAM usage:', error);
return { total: 0, used: 0, percentage: 0, free: 0 };
}
};

View File

@@ -1,76 +0,0 @@
import options from 'options';
import { module } from '../module';
import { formatTooltip, inputHandler, renderResourceLabel } from 'customModules/utils';
import { computeStorage } from './computeStorage';
import { BarBoxChild, ResourceLabelType } from 'lib/types/bar';
import { GenericResourceData } from 'lib/types/customModules/generic';
import Button from 'types/widgets/button';
import { LABEL_TYPES } from 'lib/types/defaults/bar';
import { Attribute, Child } from 'lib/types/widget';
import { FunctionPoller } from 'lib/poller/FunctionPoller';
import { Variable as TVariable } from 'types/variable';
const { label, labelType, icon, round, leftClick, rightClick, middleClick, pollingInterval } =
options.bar.customModules.storage;
const defaultStorageData = { total: 0, used: 0, percentage: 0, free: 0 };
const storageUsage = Variable<GenericResourceData>(defaultStorageData);
const storagePoller = new FunctionPoller<GenericResourceData, [TVariable<boolean>]>(
storageUsage,
[round.bind('value')],
pollingInterval.bind('value'),
computeStorage,
round,
);
storagePoller.initialize('storage');
export const Storage = (): BarBoxChild => {
const storageModule = module({
textIcon: icon.bind('value'),
label: Utils.merge(
[storageUsage.bind('value'), labelType.bind('value'), round.bind('value')],
(storage: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
return renderResourceLabel(lblTyp, storage, round);
},
),
tooltipText: labelType.bind('value').as((lblTyp) => {
return formatTooltip('Storage', lblTyp);
}),
boxClass: 'storage',
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button<Child, Attribute>) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) + 1) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
onScrollDown: {
fn: () => {
labelType.value = LABEL_TYPES[
(LABEL_TYPES.indexOf(labelType.value) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
] as ResourceLabelType;
},
},
});
},
},
});
return storageModule;
};

View File

@@ -1,20 +0,0 @@
import { Variable } from 'types/variable';
const hyprland = await Service.import('hyprland');
export const isSubmapEnabled = (submap: string, enabled: string, disabled: string): string => {
return submap !== 'default' ? enabled : disabled;
};
export const getInitialSubmap = (submapStatus: Variable<string>): void => {
let submap = hyprland.message('submap');
const newLineCarriage = /\n/g;
submap = submap.replace(newLineCarriage, '');
if (submap === 'unknown request') {
submap = 'default';
}
submapStatus.value = submap;
};

View File

@@ -1,101 +0,0 @@
const hyprland = await Service.import('hyprland');
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import { Variable as VariableType } from 'types/variable';
import { Attribute, Child } from 'lib/types/widget';
import { BarBoxChild } from 'lib/types/bar';
import { capitalizeFirstLetter } from 'lib/utils';
import { getInitialSubmap, isSubmapEnabled } from './helpers';
const {
label,
showSubmapName,
enabledIcon,
disabledIcon,
enabledText,
disabledText,
leftClick,
rightClick,
middleClick,
scrollUp,
scrollDown,
} = options.bar.customModules.submap;
const submapStatus: VariableType<string> = Variable('default');
hyprland.connect('submap', (_, currentSubmap) => {
if (currentSubmap.length === 0) {
submapStatus.value = 'default';
} else {
submapStatus.value = currentSubmap;
}
});
getInitialSubmap(submapStatus);
export const Submap = (): BarBoxChild => {
const submapModule = module({
textIcon: Utils.merge(
[submapStatus.bind('value'), enabledIcon.bind('value'), disabledIcon.bind('value')],
(status, enabled, disabled) => {
return isSubmapEnabled(status, enabled, disabled);
},
),
tooltipText: Utils.merge(
[
submapStatus.bind('value'),
enabledText.bind('value'),
disabledText.bind('value'),
showSubmapName.bind('value'),
],
(status, enabled, disabled, showSmName) => {
if (showSmName) {
return capitalizeFirstLetter(status);
}
return isSubmapEnabled(status, enabled, disabled);
},
),
boxClass: 'submap',
label: Utils.merge(
[
submapStatus.bind('value'),
enabledText.bind('value'),
disabledText.bind('value'),
showSubmapName.bind('value'),
],
(status, enabled, disabled, showSmName) => {
if (showSmName) {
return capitalizeFirstLetter(status);
}
return isSubmapEnabled(status, enabled, disabled);
},
),
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button<Child, Attribute>) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return submapModule;
};

View File

@@ -1,222 +0,0 @@
import { Option } from 'widget/settings/shared/Option';
import { Header } from 'widget/settings/shared/Header';
import options from 'options';
import Scrollable from 'types/widgets/scrollable';
import { Attribute, GtkWidget } from 'lib/types/widget';
export const CustomModuleTheme = (): Scrollable<GtkWidget, Attribute> => {
return Widget.Scrollable({
vscroll: 'automatic',
hscroll: 'automatic',
class_name: 'menu-theme-page customModules paged-container',
child: Widget.Box({
class_name: 'bar-theme-page paged-container',
vertical: true,
children: [
Header('RAM'),
Option({ opt: options.theme.bar.buttons.modules.ram.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.ram.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.ram.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.ram.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.ram.border, title: 'Border', type: 'color' }),
Header('CPU'),
Option({ opt: options.theme.bar.buttons.modules.cpu.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.cpu.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.cpu.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.cpu.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.cpu.border, title: 'Border', type: 'color' }),
Header('CPU Temperature'),
Option({ opt: options.theme.bar.buttons.modules.cpuTemp.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.cpuTemp.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.cpuTemp.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.cpuTemp.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.cpuTemp.border, title: 'Border', type: 'color' }),
Header('Storage'),
Option({ opt: options.theme.bar.buttons.modules.storage.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.storage.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.storage.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.storage.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.storage.border, title: 'Border', type: 'color' }),
Header('Netstat'),
Option({ opt: options.theme.bar.buttons.modules.netstat.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.netstat.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.netstat.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.netstat.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.netstat.border, title: 'Border', type: 'color' }),
Header('Keyboard Layout'),
Option({ opt: options.theme.bar.buttons.modules.kbLayout.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.kbLayout.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.kbLayout.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.kbLayout.border, title: 'Border', type: 'color' }),
Header('Updates'),
Option({ opt: options.theme.bar.buttons.modules.updates.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.updates.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.updates.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.updates.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.updates.border, title: 'Border', type: 'color' }),
Header('Submap'),
Option({ opt: options.theme.bar.buttons.modules.submap.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.submap.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.submap.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.submap.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.submap.border, title: 'Border', type: 'color' }),
Header('Weather'),
Option({ opt: options.theme.bar.buttons.modules.weather.icon, title: 'Icon', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.weather.text, title: 'Text', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.weather.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.weather.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.weather.border, title: 'Border', type: 'color' }),
Header('Hyprsunset'),
Option({ opt: options.theme.bar.buttons.modules.hyprsunset.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.hyprsunset.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.hyprsunset.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.hyprsunset.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.hyprsunset.border, title: 'Border', type: 'color' }),
Header('Hypridle'),
Option({ opt: options.theme.bar.buttons.modules.hypridle.text, title: 'Text', type: 'color' }),
Option({ opt: options.theme.bar.buttons.modules.hypridle.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.hypridle.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.hypridle.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.hypridle.border, title: 'Border', type: 'color' }),
Header('Power'),
Option({ opt: options.theme.bar.buttons.modules.power.icon, title: 'Icon', type: 'color' }),
Option({
opt: options.theme.bar.buttons.modules.power.background,
title: 'Label Background',
type: 'color',
}),
Option({
opt: options.theme.bar.buttons.modules.power.icon_background,
title: 'Icon Background',
subtitle:
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
type: 'color',
}),
Option({ opt: options.theme.bar.buttons.modules.power.border, title: 'Border', type: 'color' }),
],
}),
});
};

View File

@@ -1,238 +0,0 @@
import { ResourceLabelType } from 'lib/types/bar';
import { GenericResourceData, Postfix } from 'lib/types/customModules/generic';
import { InputHandlerEvents, RunAsyncCommand } from 'lib/types/customModules/utils';
import { ThrottleFn, ThrottleFnCallback } from 'lib/types/utils';
import { Attribute, Child, EventArgs } from 'lib/types/widget';
import { Binding } from 'lib/utils';
import { openMenu } from 'modules/bar/utils';
import options from 'options';
import Gdk from 'types/@girs/gdk-3.0/gdk-3.0';
import { Variable as VariableType } from 'types/variable';
import Button from 'types/widgets/button';
const { scrollSpeed } = options.bar.customModules;
const handlePostInputUpdater = (postInputUpdater?: VariableType<boolean>): void => {
if (postInputUpdater !== undefined) {
postInputUpdater.value = !postInputUpdater.value;
}
};
export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn, postInputUpdater?: VariableType<boolean>): void => {
if (cmd.startsWith('menu:')) {
const menuName = cmd.split(':')[1].trim().toLowerCase();
openMenu(events.clicked, events.event, `${menuName}menu`);
return;
}
Utils.execAsync(`bash -c "${cmd}"`)
.then((output) => {
handlePostInputUpdater(postInputUpdater);
if (fn !== undefined) {
fn(output);
}
})
.catch((err) => console.error(`Error running command "${cmd}": ${err})`));
};
export function throttleInput<T extends ThrottleFn>(func: T, limit: number): T {
let inThrottle: boolean;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
} as T;
}
export const throttledScrollHandler = (interval: number): ThrottleFn =>
throttleInput(
(cmd: string, events: EventArgs, fn: ThrottleFnCallback, postInputUpdater?: VariableType<boolean>) => {
runAsyncCommand(cmd, events, fn, postInputUpdater);
},
200 / interval,
);
const dummyVar = Variable('');
export const inputHandler = (
self: Button<Child, Attribute>,
{ onPrimaryClick, onSecondaryClick, onMiddleClick, onScrollUp, onScrollDown }: InputHandlerEvents,
postInputUpdater?: VariableType<boolean>,
): void => {
const sanitizeInput = (input: VariableType<string>): string => {
if (input === undefined) {
return '';
}
return input.value;
};
const updateHandlers = (): void => {
const interval = scrollSpeed.value;
const throttledHandler = throttledScrollHandler(interval);
self.on_primary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(
sanitizeInput(onPrimaryClick?.cmd || dummyVar),
{ clicked, event },
onPrimaryClick.fn,
postInputUpdater,
);
};
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(
sanitizeInput(onSecondaryClick?.cmd || dummyVar),
{ clicked, event },
onSecondaryClick.fn,
postInputUpdater,
);
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(
sanitizeInput(onMiddleClick?.cmd || dummyVar),
{ clicked, event },
onMiddleClick.fn,
postInputUpdater,
);
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(
sanitizeInput(onScrollUp?.cmd || dummyVar),
{ clicked, event },
onScrollUp.fn,
postInputUpdater,
);
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(
sanitizeInput(onScrollDown?.cmd || dummyVar),
{ clicked, event },
onScrollDown.fn,
postInputUpdater,
);
};
};
// Initial setup of event handlers
updateHandlers();
const sanitizeVariable = (someVar: VariableType<string> | undefined): Binding<string> => {
if (someVar === undefined || typeof someVar.bind !== 'function') {
return dummyVar.bind('value');
}
return someVar.bind('value');
};
// Re-run the update whenever scrollSpeed changes
Utils.merge(
[
scrollSpeed.bind('value'),
sanitizeVariable(onPrimaryClick),
sanitizeVariable(onSecondaryClick),
sanitizeVariable(onMiddleClick),
sanitizeVariable(onScrollUp),
sanitizeVariable(onScrollDown),
],
updateHandlers,
);
};
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;
};
export const formatSizeInKiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 1;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
export const formatSizeInMiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 2;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
export const formatSizeInGiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 3;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
export const formatSizeInTiB = (sizeInBytes: number, round: boolean): number => {
const sizeInGiB = sizeInBytes / 1024 ** 4;
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
};
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;
};
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';
};
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}%`;
};
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 '';
}
};

View File

@@ -1,52 +0,0 @@
import options from 'options';
import { module } from '../module';
import { inputHandler } from 'customModules/utils';
import Button from 'types/widgets/button';
import { getWeatherStatusTextIcon, globalWeatherVar } from 'globals/weather';
import { Attribute, Child } from 'lib/types/widget';
import { BarBoxChild } from 'lib/types/bar';
const { label, unit, leftClick, rightClick, middleClick, scrollUp, scrollDown } = options.bar.customModules.weather;
export const Weather = (): BarBoxChild => {
const weatherModule = module({
textIcon: Utils.merge([globalWeatherVar.bind('value')], (wthr) => {
const weatherStatusIcon = getWeatherStatusTextIcon(wthr);
return weatherStatusIcon;
}),
tooltipText: globalWeatherVar.bind('value').as((v) => `Weather Status: ${v.current.condition.text}`),
boxClass: 'weather-custom',
label: Utils.merge([globalWeatherVar.bind('value'), unit.bind('value')], (wthr, unt) => {
if (unt === 'imperial') {
return `${Math.ceil(wthr.current.temp_f)}° F`;
} else {
return `${Math.ceil(wthr.current.temp_c)}° C`;
}
}),
showLabelBinding: label.bind('value'),
props: {
setup: (self: Button<Child, Attribute>) => {
inputHandler(self, {
onPrimaryClick: {
cmd: leftClick,
},
onSecondaryClick: {
cmd: rightClick,
},
onMiddleClick: {
cmd: middleClick,
},
onScrollUp: {
cmd: scrollUp,
},
onScrollDown: {
cmd: scrollDown,
},
});
},
},
});
return weatherModule;
};

View File

@@ -1,38 +0,0 @@
import Service from 'resource:///com/github/Aylur/ags/service.js';
import App from 'resource:///com/github/Aylur/ags/app.js';
import { monitorFile } from 'resource:///com/github/Aylur/ags/utils.js';
import Gio from 'gi://Gio';
import { FileInfo } from 'types/@girs/gio-2.0/gio-2.0.cjs';
class DirectoryMonitorService extends Service {
static {
Service.register(this, {}, {});
}
constructor() {
super();
this.recursiveDirectoryMonitor(`${App.configDir}/scss`);
}
recursiveDirectoryMonitor(directoryPath: string): void {
monitorFile(directoryPath, (_, eventType) => {
if (eventType === Gio.FileMonitorEvent.CHANGES_DONE_HINT) {
this.emit('changed');
}
});
const directory = Gio.File.new_for_path(directoryPath);
const enumerator = directory.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
let fileInfo: FileInfo;
while ((fileInfo = enumerator.next_file(null) as FileInfo) !== null) {
const childPath = directoryPath + '/' + fileInfo.get_name();
if (fileInfo.get_file_type() === Gio.FileType.DIRECTORY) {
this.recursiveDirectoryMonitor(childPath);
}
}
}
}
const service = new DirectoryMonitorService();
export default service;

21
env.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
declare const SRC: string;
declare module 'inline:*' {
const content: string;
export default content;
}
declare module '*.scss' {
const content: string;
export default content;
}
declare module '*.blp' {
const content: string;
export default content;
}
declare module '*.css' {
const content: string;
export default content;
}

1
external/ags-types vendored

Submodule external/ags-types deleted from 87b5046791

View File

@@ -1,6 +0,0 @@
import { Variable as VariableType } from 'types/variable';
type GlobalEventBoxes = {
[key: string]: unknown;
};
export const globalEventBoxes: VariableType<GlobalEventBoxes> = Variable({});

View File

@@ -1,15 +0,0 @@
export const WIFI_STATUS_MAP = {
unknown: 'Status Unknown',
unmanaged: 'Unmanaged',
unavailable: 'Unavailable',
disconnected: 'Disconnected',
prepare: 'Preparing Connecting',
config: 'Connecting',
need_auth: 'Needs Authentication',
ip_config: 'Requesting IP',
ip_check: 'Checking Access',
secondaries: 'Waiting on Secondaries',
activated: 'Connected',
deactivating: 'Disconnecting',
failed: 'Connection Failed',
} as const;

View File

@@ -1,46 +0,0 @@
const notifs = await Service.import('notifications');
import icons from 'modules/icons/index';
import options from 'options';
import { Notification } from 'types/service/notifications';
const { clearDelay } = options.notifications;
export const removingNotifications = Variable<boolean>(false);
export const getNotificationIcon = (app_name: string, app_icon: string, app_entry: string): string => {
let icon: string = icons.fallback.notification;
if (Utils.lookUpIcon(app_name) || Utils.lookUpIcon(app_name.toLowerCase() || '')) {
icon = Utils.lookUpIcon(app_name)
? app_name
: Utils.lookUpIcon(app_name.toLowerCase())
? app_name.toLowerCase()
: '';
}
if (Utils.lookUpIcon(app_icon) && icon === '') {
icon = app_icon;
}
if (Utils.lookUpIcon(app_entry || '') && icon === '') {
icon = app_entry || '';
}
return icon;
};
export const clearNotifications = async (notifications: Notification[], delay: number): Promise<void> => {
removingNotifications.value = true;
for (const notif of notifications) {
notif.close();
await new Promise((resolve) => setTimeout(resolve, delay));
}
removingNotifications.value = false;
};
const clearAllNotifications = async (): Promise<void> => {
clearNotifications(notifs.notifications, clearDelay.value);
};
globalThis['removingNotifications'] = removingNotifications;
globalThis['clearAllNotifications'] = clearAllNotifications;

View File

@@ -1,7 +0,0 @@
const systemtray = await Service.import('systemtray');
globalThis.getSystrayItems = (): string => {
return systemtray.items.map((systrayItem) => systrayItem.id).join('\n');
};
export { getSystrayItems };

View File

@@ -1,45 +0,0 @@
import options from 'options';
import Gio from 'gi://Gio';
import { bash, Notify } from 'lib/utils';
import icons from 'lib/icons';
import { filterConfigForThemeOnly, loadJsonFile, saveConfigToFile } from 'widget/settings/shared/FileChooser';
const { restartCommand } = options.hyprpanel;
export const hexColorPattern = /^#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
globalThis.useTheme = (filePath: string): void => {
const importedConfig = loadJsonFile(filePath);
if (!importedConfig) {
return;
}
Notify({
summary: `Importing Theme`,
body: `Importing: ${filePath}`,
iconName: icons.ui.info,
timeout: 7000,
});
const tmpConfigFile = Gio.File.new_for_path(`${TMP}/config.json`);
const optionsConfigFile = Gio.File.new_for_path(OPTIONS);
const [tmpSuccess, tmpContent] = tmpConfigFile.load_contents(null);
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
if (!tmpSuccess || !optionsSuccess) {
console.error('Failed to read existing configuration files.');
return;
}
let tmpConfig = JSON.parse(new TextDecoder('utf-8').decode(tmpContent));
let optionsConfig = JSON.parse(new TextDecoder('utf-8').decode(optionsContent));
const filteredConfig = filterConfigForThemeOnly(importedConfig);
tmpConfig = { ...tmpConfig, ...filteredConfig };
optionsConfig = { ...optionsConfig, ...filteredConfig };
saveConfigToFile(tmpConfig, `${TMP}/config.json`);
saveConfigToFile(optionsConfig, OPTIONS);
bash(restartCommand.value);
};

View File

@@ -1,22 +0,0 @@
import options from 'options';
globalThis.isWindowVisible = (windowName: string): boolean => {
const appWindow = App.getWindow(windowName);
if (appWindow === undefined) {
return false;
}
return appWindow.visible;
};
globalThis.setLayout = (layout: string): string => {
try {
const layoutJson = JSON.parse(layout);
const { layouts } = options.bar;
layouts.value = layoutJson;
return 'Successfully updated layout.';
} catch (error) {
return `Failed to set layout: ${error}`;
}
};

View File

@@ -1,22 +0,0 @@
import GLib from 'gi://GLib?version=2.0';
import { Notify } from 'lib/utils';
import options from 'options';
import Wallpaper from 'services/Wallpaper';
const { EXISTS, IS_REGULAR } = GLib.FileTest;
const { enable: enableWallpaper, image } = options.wallpaper;
globalThis.setWallpaper = (filePath: string): void => {
if (!(GLib.file_test(filePath, EXISTS) && GLib.file_test(filePath, IS_REGULAR))) {
Notify({
summary: 'Failed to set Wallpaper',
body: 'The input file is not a valid wallpaper.',
});
}
image.value = filePath;
if (enableWallpaper.value) {
Wallpaper.set(filePath);
}
};

View File

@@ -1,147 +0,0 @@
import { isHexColor } from 'globals/variables';
import { Variable } from 'resource:///com/github/Aylur/ags/variable.js';
import { MkOptionsResult } from './types/options';
type OptProps = {
persistent?: boolean;
};
export class Opt<T = unknown> extends Variable<T> {
static {
Service.register(this);
}
constructor(initial: T, { persistent = false }: OptProps = {}) {
super(initial);
this.initial = initial;
this.persistent = persistent;
}
initial: T;
id = '';
persistent: boolean;
toString(): string {
return `${this.value}`;
}
toJSON(): string {
return `opt:${this.value}`;
}
getValue = (): T => {
return super.getValue();
};
init(cacheFile: string): void {
const cacheV = JSON.parse(Utils.readFile(cacheFile) || '{}')[this.id];
if (cacheV !== undefined) this.value = cacheV;
this.connect('changed', () => {
const cache = JSON.parse(Utils.readFile(cacheFile) || '{}');
cache[this.id] = this.value;
Utils.writeFileSync(JSON.stringify(cache, null, 2), cacheFile);
});
}
reset(): string | undefined {
if (this.persistent) return;
if (JSON.stringify(this.value) !== JSON.stringify(this.initial)) {
this.value = this.initial;
return this.id;
}
}
doResetColor(): string | undefined {
if (this.persistent) return;
const isColor = isHexColor(this.value as string);
if (JSON.stringify(this.value) !== JSON.stringify(this.initial) && isColor) {
this.value = this.initial;
return this.id;
}
return;
}
}
export const opt = <T>(initial: T, opts?: OptProps): Opt<T> => new Opt(initial, opts);
const getOptions = (object: Record<string, unknown>, path = ''): Opt[] => {
return Object.keys(object).flatMap((key) => {
const obj = object[key];
const id = path ? path + '.' + key : key;
if (obj instanceof Variable) {
const optValue = obj as Opt;
optValue.id = id;
return optValue;
}
if (typeof obj === 'object' && obj !== null) {
return getOptions(obj as Record<string, unknown>, id); // Recursively process nested objects
}
return [];
});
};
export function mkOptions<T extends object>(
cacheFile: string,
object: T,
confFile: string = 'config.json',
): T & MkOptionsResult {
for (const opt of getOptions(object as Record<string, unknown>)) opt.init(cacheFile);
Utils.ensureDirectory(cacheFile.split('/').slice(0, -1).join('/'));
const configFile = `${TMP}/${confFile}`;
const values = getOptions(object as Record<string, unknown>).reduce(
(obj, { id, value }) => ({ [id]: value, ...obj }),
{},
);
Utils.writeFileSync(JSON.stringify(values, null, 2), configFile);
Utils.monitorFile(configFile, () => {
const cache = JSON.parse(Utils.readFile(configFile) || '{}');
for (const opt of getOptions(object as Record<string, unknown>)) {
if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value)) opt.value = cache[opt.id];
}
});
function sleep(ms = 0): Promise<T> {
return new Promise((r) => setTimeout(r, ms));
}
const reset = async (
[opt, ...list] = getOptions(object as Record<string, unknown>),
id = opt?.reset(),
): Promise<Array<string>> => {
if (!opt) return sleep().then(() => []);
return id ? [id, ...(await sleep(50).then(() => reset(list)))] : await sleep().then(() => reset(list));
};
const resetTheme = async (
[opt, ...list] = getOptions(object as Record<string, unknown>),
id = opt?.doResetColor(),
): Promise<Array<string>> => {
if (!opt) return sleep().then(() => []);
return id
? [id, ...(await sleep(50).then(() => resetTheme(list)))]
: await sleep().then(() => resetTheme(list));
};
return Object.assign(object, {
configFile,
array: () => getOptions(object as Record<string, unknown>),
async reset() {
return (await reset()).join('\n');
},
async resetTheme() {
return (await resetTheme()).join('\n');
},
handler(deps: string[], callback: () => void) {
for (const opt of getOptions(object as Record<string, unknown>)) {
if (deps.some((i) => opt.id.startsWith(i))) opt.connect('changed', callback);
}
},
});
}

View File

@@ -1,16 +0,0 @@
import GLib from 'gi://GLib?version=2.0';
declare global {
const OPTIONS: string;
const TMP: string;
const USER: string;
}
Object.assign(globalThis, {
OPTIONS: `${GLib.get_user_cache_dir()}/ags/hyprpanel/options.json`,
TMP: `${GLib.get_tmp_dir()}/ags/hyprpanel`,
USER: GLib.get_user_name(),
});
Utils.ensureDirectory(TMP);
App.addIcons(`${App.configDir}/assets`);

View File

@@ -1,28 +0,0 @@
import { MprisPlayer } from 'types/service/mpris';
const mpris = await Service.import('mpris');
export const getCurrentPlayer = (activePlayer: MprisPlayer = mpris.players[0]): MprisPlayer => {
const statusOrder = {
Playing: 1,
Paused: 2,
Stopped: 3,
};
if (mpris.players.length === 0) {
return mpris.players[0];
}
const isPlaying = mpris.players.some((p: MprisPlayer) => p.play_back_status === 'Playing');
const playerStillExists = mpris.players.some((p) => activePlayer.bus_name === p.bus_name);
const nextPlayerUp = mpris.players.sort(
(a: MprisPlayer, b: MprisPlayer) => statusOrder[a.play_back_status] - statusOrder[b.play_back_status],
)[0];
if (isPlaying || !playerStillExists) {
return nextPlayerUp;
}
return activePlayer;
};

View File

@@ -1,12 +0,0 @@
import { Notification } from 'types/service/notifications';
export const filterNotifications = (notifications: Notification[], filter: string[]): Notification[] => {
const notifFilter = new Set(filter.map((name: string) => name.toLowerCase().replace(/\s+/g, '_')));
const filteredNotifications = notifications.filter((notif: Notification) => {
const normalizedAppName = notif.app_name.toLowerCase().replace(/\s+/g, '_');
return !notifFilter.has(normalizedAppName);
});
return filteredNotifications;
};

View File

@@ -1,5 +0,0 @@
import { NetstatLabelType, ResourceLabelType } from '../bar';
export const LABEL_TYPES: ResourceLabelType[] = ['used/total', 'used', 'free', 'percentage'];
export const NETWORK_LABEL_TYPES: NetstatLabelType[] = ['full', 'in', 'out'];

View File

@@ -1,12 +0,0 @@
import { WindowProps } from 'types/widgets/window';
import { GtkWidget, Transition } from './widget';
import { Binding } from 'types/service';
export type DropdownMenuProps = {
name: string;
child: GtkWidget;
layout?: string;
transition?: Transition | Binding<Transition>;
exclusivity?: Exclusivity;
fixed?: boolean;
} & WindowProps;

View File

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

29
lib/types/widget.d.ts vendored
View File

@@ -1,29 +0,0 @@
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0';
import Box from 'types/widgets/box';
export type Exclusivity = 'normal' | 'ignore' | 'exclusive';
export type Anchor = 'left' | 'right' | 'top' | 'down';
export type Transition = 'none' | 'crossfade' | 'slide_right' | 'slide_left' | 'slide_up' | 'slide_down';
export type Layouts =
| 'center'
| 'top'
| 'top-right'
| 'top-center'
| 'top-left'
| 'bottom-left'
| 'bottom-center'
| 'bottom-right';
export type Attribute = unknown;
export type Child = Gtk.Widget;
export type GtkWidget = Gtk.Widget;
export type BoxWidget = Box<GtkWidget, Child>;
export type GButton = Gtk.Button;
export type GBox = Gtk.Box;
export type GLabel = Gtk.Label;
export type GCenterBox = Gtk.Box;
export type EventHandler<Self> = (self: Self, event: Gdk.Event) => boolean | unknown;
export type EventArgs = { clicked: Button<Child, Attribute>; event: Gdk.Event };

View File

@@ -1,246 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type Application } from 'types/service/applications';
import { BarModule, NotificationAnchor } from './types/options';
import { OSDAnchor } from 'lib/types/options';
import icons, { substitutes } from './icons';
import Gtk from 'gi://Gtk?version=3.0';
import Gdk from 'gi://Gdk';
import GLib from 'gi://GLib?version=2.0';
import GdkPixbuf from 'gi://GdkPixbuf';
import { NotificationArgs } from 'types/utils/notify';
import { SubstituteKeys } from './types/utils';
import { Window } from 'types/@girs/gtk-3.0/gtk-3.0.cjs';
import { namedColors } from './constants/colors';
import { distroIcons } from './constants/distro';
import { distro } from './variables';
const battery = await Service.import('battery');
import options from 'options';
export type Binding<T> = import('types/service').Binding<any, any, T>;
/**
* Retrieves all unique layout items from the bar options.
*
* @returns An array of unique layout items.
*/
export const getLayoutItems = (): BarModule[] => {
const { layouts } = options.bar;
const itemsInLayout: BarModule[] = [];
Object.keys(layouts.value).forEach((monitor) => {
const leftItems = layouts.value[monitor].left;
const rightItems = layouts.value[monitor].right;
const middleItems = layouts.value[monitor].middle;
itemsInLayout.push(...leftItems);
itemsInLayout.push(...middleItems);
itemsInLayout.push(...rightItems);
});
return [...new Set(itemsInLayout)];
};
/**
* @returns substitute icon || name || fallback icon
*/
export function icon(name: string | null, fallback = icons.missing): string {
const validateSubstitute = (name: string): name is SubstituteKeys => name in substitutes;
if (!name) return fallback || '';
if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name;
let icon: string = name;
if (validateSubstitute(name)) {
icon = substitutes[name];
}
if (Utils.lookUpIcon(icon)) return icon;
print(`no icon substitute "${icon}" for "${name}", fallback: "${fallback}"`);
return fallback;
}
/**
* @returns execAsync(["bash", "-c", cmd])
*/
export async function bash(strings: TemplateStringsArray | string, ...values: unknown[]): Promise<string> {
const cmd =
typeof strings === 'string' ? strings : strings.flatMap((str, i) => str + `${values[i] ?? ''}`).join('');
return Utils.execAsync(['bash', '-c', cmd]).catch((err) => {
console.error(cmd, err);
return '';
});
}
/**
* @returns execAsync(cmd)
*/
export async function sh(cmd: string | string[]): Promise<string> {
return Utils.execAsync(cmd).catch((err) => {
console.error(typeof cmd === 'string' ? cmd : cmd.join(' '), err);
return '';
});
}
export function forMonitors(widget: (monitor: number) => Gtk.Window): Window[] {
const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
return range(n, 0).flatMap(widget);
}
/**
* @returns [start...length]
*/
export function range(length: number, start = 1): number[] {
return Array.from({ length }, (_, i) => i + start);
}
/**
* @returns true if all of the `bins` are found
*/
export function dependencies(...bins: string[]): boolean {
const missing = bins.filter((bin) =>
Utils.exec({
cmd: `which ${bin}`,
out: () => false,
err: () => true,
}),
);
if (missing.length > 0) {
console.warn(Error(`missing dependencies: ${missing.join(', ')}`));
Notify({
summary: 'Dependencies not found!',
body: `The following dependencies are missing: ${missing.join(', ')}`,
iconName: icons.ui.warning,
timeout: 7000,
});
}
return missing.length === 0;
}
/**
* run app detached
*/
export function launchApp(app: Application): void {
const exe = app.executable
.split(/\s+/)
.filter((str) => !str.startsWith('%') && !str.startsWith('@'))
.join(' ');
bash(`${exe} &`);
app.frequency += 1;
}
/**
* to use with drag and drop
*/
export function createSurfaceFromWidget(widget: Gtk.Widget): GdkPixbuf.Pixbuf {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cairo = imports.gi.cairo as any;
const alloc = widget.get_allocation();
const surface = new cairo.ImageSurface(cairo.Format.ARGB32, alloc.width, alloc.height);
const cr = new cairo.Context(surface);
cr.setSourceRGBA(255, 255, 255, 0);
cr.rectangle(0, 0, alloc.width, alloc.height);
cr.fill();
widget.draw(cr);
return surface;
}
/**
* Ensure that the provided filepath is a valid image
*/
export const isAnImage = (imgFilePath: string): boolean => {
try {
GdkPixbuf.Pixbuf.new_from_file(imgFilePath);
return true;
} catch (error) {
console.error(error);
return false;
}
};
export const Notify = (notifPayload: NotificationArgs): void => {
let command = 'notify-send';
command += ` "${notifPayload.summary} "`;
if (notifPayload.body) command += ` "${notifPayload.body}" `;
if (notifPayload.appName) command += ` -a "${notifPayload.appName}"`;
if (notifPayload.iconName) command += ` -i "${notifPayload.iconName}"`;
if (notifPayload.urgency) command += ` -u "${notifPayload.urgency}"`;
if (notifPayload.timeout !== undefined) command += ` -t ${notifPayload.timeout}`;
if (notifPayload.category) command += ` -c "${notifPayload.category}"`;
if (notifPayload.transient) command += ` -e`;
if (notifPayload.id !== undefined) command += ` -r ${notifPayload.id}`;
Utils.execAsync(command);
};
export const getPosition = (pos: NotificationAnchor | OSDAnchor): ('top' | 'bottom' | 'left' | 'right')[] => {
const positionMap: { [key: string]: ('top' | 'bottom' | 'left' | 'right')[] } = {
top: ['top'],
'top right': ['top', 'right'],
'top left': ['top', 'left'],
bottom: ['bottom'],
'bottom right': ['bottom', 'right'],
'bottom left': ['bottom', 'left'],
right: ['right'],
left: ['left'],
};
return positionMap[pos] || ['top'];
};
export const isValidGjsColor = (color: string): boolean => {
const colorLower = color.toLowerCase().trim();
if (namedColors.has(colorLower)) {
return true;
}
const hexColorRegex = /^#(?:[a-fA-F0-9]{3,4}|[a-fA-F0-9]{6,8})$/;
const rgbRegex = /^rgb\(\s*(\d{1,3}%?\s*,\s*){2}\d{1,3}%?\s*\)$/;
const rgbaRegex = /^rgba\(\s*(\d{1,3}%?\s*,\s*){3}(0|1|0?\.\d+)\s*\)$/;
if (hexColorRegex.test(color)) {
return true;
}
if (rgbRegex.test(colorLower) || rgbaRegex.test(colorLower)) {
return true;
}
return false;
};
export const capitalizeFirstLetter = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
export function getDistroIcon(): string {
const icon = distroIcons.find(([id]) => id === distro.id);
return icon ? icon[1] : ''; // default icon if not found
}
export const warnOnLowBattery = (): void => {
battery.connect('notify::percent', () => {
const { lowBatteryThreshold, lowBatteryNotification, lowBatteryNotificationText, lowBatteryNotificationTitle } =
options.menus.power;
if (!lowBatteryNotification.value || battery.charging) return;
const lowThreshold = lowBatteryThreshold.value;
if (battery.percent === lowThreshold || battery.percent === lowThreshold / 2) {
Notify({
summary: lowBatteryNotificationTitle.value.replace('/$POWER_LEVEL/g', battery.percent.toString()),
body: lowBatteryNotificationText.value.replace('/$POWER_LEVEL/g', battery.percent.toString()),
iconName: icons.ui.warning,
urgency: 'critical',
timeout: 7000,
});
}
});
};

View File

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

68
main.ts
View File

@@ -1,68 +0,0 @@
import 'lib/session';
import 'scss/style';
import 'globals/useTheme';
import 'globals/wallpaper';
import 'globals/systray';
import 'globals/dropdown.js';
import 'globals/utilities';
const hyprland = await Service.import('hyprland');
import { Bar } from 'modules/bar/Bar';
import MenuWindows from './modules/menus/main.js';
import SettingsDialog from 'widget/settings/SettingsDialog';
import Notifications from './modules/notifications/index.js';
import { bash, forMonitors, warnOnLowBattery } from 'lib/utils';
import options from 'options.js';
import OSD from 'modules/osd/index';
App.config({
onConfigParsed: () => [Utils.execAsync(`python3 ${App.configDir}/services/bluetooth.py`), warnOnLowBattery()],
windows: [...MenuWindows, Notifications(), SettingsDialog(), ...forMonitors(Bar), OSD()],
closeWindowDelay: {
sideright: 350,
launcher: 350,
bar0: 350,
},
});
/**
* Function to determine if the current OS is NixOS by parsing /etc/os-release.
* @returns True if NixOS, false otherwise.
*/
const isNixOS = (): boolean => {
try {
const osRelease = Utils.exec('cat /etc/os-release').toString();
const idMatch = osRelease.match(/^ID\s*=\s*"?([^"\n]+)"?/m);
if (idMatch && idMatch[1].toLowerCase() === 'nixos') {
return true;
}
return false;
} catch (error) {
console.error('Error detecting OS:', error);
return false;
}
};
/**
* Function to generate the appropriate restart command based on the OS.
* @returns The modified or original restart command.
*/
const getRestartCommand = (): string => {
const isNix = isNixOS();
const command = options.hyprpanel.restartCommand.value;
if (isNix) {
return command.replace(/\bags\b/g, 'hyprpanel');
}
return command;
};
hyprland.connect('monitor-added', () => {
if (options.hyprpanel.restartAgs.value) {
const restartAgsCommand = getRestartCommand();
bash(restartAgsCommand);
}
});

View File

@@ -1,8 +0,0 @@
#!/bin/bash
makepkg -si
echo "Cleaning up build files..."
rm -rf -- pkg src *.pkg.tar.* *.tar.gz
echo "Build and installation completed successfully. All generated files have been cleaned up."

64
meson.build Normal file
View File

@@ -0,0 +1,64 @@
project('hyprpanel')
bindir = get_option('prefix') / get_option('bindir')
datadir = get_option('prefix') / get_option('datadir') / 'hyprpanel'
ags = find_program('ags', required: true)
find_program('gjs', required: true)
src_file_list_process = run_command(
'find',
'src',
'-type', 'f',
'(',
'-name', '*.ts',
'-o',
'-name', '*.tsx',
'-o',
'-name', '*.scss',
')',
)
if src_file_list_process.returncode() != 0
error('Failed to find source files.')
endif
src_file_list = src_file_list_process.stdout().split('\n')
all_sources = []
foreach file : src_file_list
file_stripped = file.strip()
if file_stripped != ''
all_sources += meson.project_source_root() / file_stripped
endif
endforeach
custom_target(
'hyprpanel_bundle',
input: all_sources,
command: [
ags,
'bundle',
meson.project_source_root() / 'app.ts',
'@OUTPUT@',
'--src', meson.project_source_root(),
],
output: 'hyprpanel.js',
install: true,
install_dir: datadir,
)
configure_file(
input: 'scripts/hyprpanel_launcher.sh.in',
output: 'hyprpanel',
configuration: {'DATADIR': datadir},
install: true,
install_dir: bindir,
install_mode: 'rwxr-xr-x',
)
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')

View File

@@ -1,301 +0,0 @@
const hyprland = await Service.import('hyprland');
import {
Menu,
Workspaces,
ClientTitle,
Media,
Notifications,
Volume,
Network,
Bluetooth,
BatteryLabel,
Clock,
SysTray,
// Custom Modules
Ram,
Cpu,
CpuTemp,
Storage,
Netstat,
KbInput,
Updates,
Submap,
Weather,
Power,
Hyprsunset,
Hypridle,
} from './Exports';
import { BarItemBox as WidgetContainer } from '../shared/barItemBox.js';
import options from 'options';
import Gdk from 'gi://Gdk?version=3.0';
import Button from 'types/widgets/button.js';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
import './SideEffects';
import { BarLayout, BarLayouts, WindowLayer } from 'lib/types/options.js';
import { Attribute, Child } from 'lib/types/widget.js';
import Window from 'types/widgets/window.js';
const { layouts } = options.bar;
const { location } = options.theme.bar;
const { location: borderLocation } = options.theme.bar.border;
const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLayout => {
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
const wildcard = Object.keys(layouts).find((key) => key === '*');
if (matchingKey) {
return layouts[matchingKey];
}
if (wildcard) {
return layouts[wildcard];
}
return {
left: ['dashboard', 'workspaces', 'windowtitle'],
middle: ['media'],
right: ['volume', 'network', 'bluetooth', 'battery', 'systray', 'clock', 'notifications'],
};
};
const isLayoutEmpty = (layout: BarLayout): boolean => {
const isLeftSectionEmpty = !Array.isArray(layout.left) || layout.left.length === 0;
const isRightSectionEmpty = !Array.isArray(layout.right) || layout.right.length === 0;
const isMiddleSectionEmpty = !Array.isArray(layout.middle) || layout.middle.length === 0;
return isLeftSectionEmpty && isRightSectionEmpty && isMiddleSectionEmpty;
};
const widget = {
battery: (): Button<Child, Attribute> => WidgetContainer(BatteryLabel()),
dashboard: (): Button<Child, Attribute> => WidgetContainer(Menu()),
workspaces: (monitor: number): Button<Child, Attribute> => WidgetContainer(Workspaces(monitor)),
windowtitle: (): Button<Child, Attribute> => WidgetContainer(ClientTitle()),
media: (): Button<Child, Attribute> => WidgetContainer(Media()),
notifications: (): Button<Child, Attribute> => WidgetContainer(Notifications()),
volume: (): Button<Child, Attribute> => WidgetContainer(Volume()),
network: (): Button<Child, Attribute> => WidgetContainer(Network()),
bluetooth: (): Button<Child, Attribute> => WidgetContainer(Bluetooth()),
clock: (): Button<Child, Attribute> => WidgetContainer(Clock()),
systray: (): Button<Child, Attribute> => WidgetContainer(SysTray()),
ram: (): Button<Child, Attribute> => WidgetContainer(Ram()),
cpu: (): Button<Child, Attribute> => WidgetContainer(Cpu()),
cputemp: (): Button<Child, Attribute> => WidgetContainer(CpuTemp()),
storage: (): Button<Child, Attribute> => WidgetContainer(Storage()),
netstat: (): Button<Child, Attribute> => WidgetContainer(Netstat()),
kbinput: (): Button<Child, Attribute> => WidgetContainer(KbInput()),
updates: (): Button<Child, Attribute> => WidgetContainer(Updates()),
submap: (): Button<Child, Attribute> => WidgetContainer(Submap()),
weather: (): Button<Child, Attribute> => WidgetContainer(Weather()),
power: (): Button<Child, Attribute> => WidgetContainer(Power()),
hyprsunset: (): Button<Child, Attribute> => WidgetContainer(Hyprsunset()),
hypridle: (): Button<Child, Attribute> => WidgetContainer(Hypridle()),
};
type GdkMonitors = {
[key: string]: {
key: string;
model: string;
used: boolean;
};
};
function getGdkMonitors(): GdkMonitors {
const display = Gdk.Display.get_default();
if (display === null) {
console.error('Failed to get Gdk display.');
return {};
}
const numGdkMonitors = display.get_n_monitors();
const gdkMonitors: GdkMonitors = {};
for (let i = 0; i < numGdkMonitors; i++) {
const curMonitor = display.get_monitor(i);
if (curMonitor === null) {
console.warn(`Monitor at index ${i} is null.`);
continue;
}
const model = curMonitor.get_model() || '';
const geometry = curMonitor.get_geometry();
const scaleFactor = curMonitor.get_scale_factor();
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
gdkMonitors[i] = { key, model, used: false };
}
return gdkMonitors;
}
/**
* NOTE: Some more funky stuff being done by GDK.
* We render windows/bar based on the monitor ID. So if you have 3 monitors, then your
* monitor IDs will be [0, 1, 2]. Hyprland will NEVER change what ID belongs to what monitor.
*
* So if hyprland determines id 0 = DP-1, even after you unplug, shut off or restart your monitor,
* the id 0 will ALWAYS be DP-1.
*
* However, GDK (the righteous genius that it is) will change the order of ID anytime your monitor
* setup is changed. So if you unplug your monitor and plug it back it, it now becomes the last id.
* So if DP-1 was id 0 and you unplugged it, it will reconfigure to id 2. This sucks because now
* there's a mismtach between what GDK determines the monitor is at id 2 and what Hyprland determines
* is at id 2.
*
* So for that reason, we need to redirect the input `monitor` that the Bar module takes in, to the
* proper Hyprland monitor. So when monitor id 0 comes in, we need to find what the id of that monitor
* is being determined as by Hyprland so the bars show up on the right monitors.
*
* Since GTK3 doesn't contain connection names and only monitor models, we have to make the best guess
* in the case that there are multiple models in the same resolution with the same scale. We find the
* 'right' monitor by checking if the model matches along with the resolution and scale. If monitor at
* ID 0 for GDK is being reported as 'MSI MAG271CQR' we find the same model in the Hyprland monitor list
* and check if the resolution and scaling is the same... if it is then we determine it's a match.
*
* The edge-case that we just can't handle is if you have the same monitors in the same resolution at the same
* scale. So if you've got 2 'MSI MAG271CQR' monitors at 2560x1440 at scale 1, then we just match the first
* monitor in the list as the first match and then the second 'MSI MAG271CQR' as a match in the 2nd iteration.
* You may have the bar showing up on the wrong one in this case because we don't know what the connector id
* is of either of these monitors (DP-1, DP-2) which are unique values - as these are only in GTK4.
*
* Keep in mind though, this is ONLY an issue if you change your monitor setup by plugging in a new one, restarting
* an existing one or shutting it off.
*
* If your monitors aren't changed in the current session you're in then none of this safeguarding is relevant.
*
* Fun stuff really... :facepalm:
*/
const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set<number>): number => {
const gdkMonitors = getGdkMonitors();
if (Object.keys(gdkMonitors).length === 0) {
console.error('No GDK monitors were found.');
return monitor;
}
// Get the GDK monitor for the given monitor index
const gdkMonitor = gdkMonitors[monitor];
// First pass: Strict matching including the monitor index (i.e., hypMon.id === monitor + resolution+scale criteria)
const directMatch = hyprland.monitors.find((hypMon) => {
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id) && hypMon.id === monitor;
});
if (directMatch) {
usedHyprlandMonitors.add(directMatch.id);
return directMatch.id;
}
// Second pass: Relaxed matching without considering the monitor index
const hyprlandMonitor = hyprland.monitors.find((hypMon) => {
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id);
});
if (hyprlandMonitor) {
usedHyprlandMonitors.add(hyprlandMonitor.id);
return hyprlandMonitor.id;
}
// Fallback: Find the first available monitor ID that hasn't been used
const fallbackMonitor = hyprland.monitors.find((hypMon) => !usedHyprlandMonitors.has(hypMon.id));
if (fallbackMonitor) {
usedHyprlandMonitors.add(fallbackMonitor.id);
return fallbackMonitor.id;
}
// Ensure we return a valid monitor ID that actually exists
for (let i = 0; i < hyprland.monitors.length; i++) {
if (!usedHyprlandMonitors.has(i)) {
usedHyprlandMonitors.add(i);
return i;
}
}
// As a last resort, return the original monitor index if no unique monitor can be found
console.warn(`Returning original monitor index as a last resort: ${monitor}`);
return monitor;
};
export const Bar = (() => {
const usedHyprlandMonitors = new Set<number>();
return (monitor: number): Window<Child, Attribute> => {
const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors);
return Widget.Window({
name: `bar-${hyprlandMonitor}`,
class_name: 'bar',
monitor,
visible: layouts.bind('value').as(() => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
return !isLayoutEmpty(foundLayout);
}),
anchor: location.bind('value').as((ln) => [ln, 'left', 'right']),
exclusivity: 'exclusive',
layer: Utils.merge(
[options.theme.bar.layer.bind('value'), options.tear.bind('value')],
(barLayer: WindowLayer, tear: boolean) => {
if (tear && barLayer === 'overlay') {
return 'top';
}
return barLayer;
},
),
child: Widget.Box({
class_name: 'bar-panel-container',
child: Widget.CenterBox({
class_name: borderLocation
.bind('value')
.as((brdrLcn) => (brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel')),
css: 'padding: 1px',
startWidget: Widget.Box({
class_name: 'box-left',
hexpand: true,
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
self.children = foundLayout.left
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
});
},
}),
centerWidget: Widget.Box({
class_name: 'box-center',
hpack: 'center',
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
self.children = foundLayout.middle
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
});
},
}),
endWidget: Widget.Box({
class_name: 'box-right',
hpack: 'end',
setup: (self) => {
self.hook(layouts, (self) => {
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
self.children = foundLayout.right
.filter((mod) => Object.keys(widget).includes(mod))
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
});
},
}),
}),
}),
});
};
})();

View File

@@ -1,53 +0,0 @@
import { Menu } from './menu/index';
import { Workspaces } from './workspaces/index';
import { ClientTitle } from './window_title/index';
import { Media } from './media/index';
import { Notifications } from './notifications/index';
import { Volume } from './volume/index';
import { Network } from './network/index';
import { Bluetooth } from './bluetooth/index';
import { BatteryLabel } from './battery/index';
import { Clock } from './clock/index';
import { SysTray } from './systray/index';
// Custom Modules
import { Ram } from '../../customModules/ram/index';
import { Cpu } from '../../customModules/cpu/index';
import { CpuTemp } from 'customModules/cputemp/index';
import { Storage } from 'customModules/storage/index';
import { Netstat } from 'customModules/netstat/index';
import { KbInput } from 'customModules/kblayout/index';
import { Updates } from 'customModules/updates/index';
import { Submap } from 'customModules/submap/index';
import { Weather } from 'customModules/weather/index';
import { Power } from 'customModules/power/index';
import { Hyprsunset } from 'customModules/hyprsunset/index';
import { Hypridle } from 'customModules/hypridle/index';
export {
Menu,
Workspaces,
ClientTitle,
Media,
Notifications,
Volume,
Network,
Bluetooth,
BatteryLabel,
Clock,
SysTray,
// Custom Modules
Ram,
Cpu,
CpuTemp,
Storage,
Netstat,
KbInput,
Updates,
Submap,
Weather,
Power,
Hyprsunset,
Hypridle,
};

View File

@@ -1,29 +0,0 @@
import options from 'options';
const { showIcon, showTime } = options.bar.clock;
showIcon.connect('changed', () => {
if (!showTime.value && !showIcon.value) {
showTime.value = true;
}
});
showTime.connect('changed', () => {
if (!showTime.value && !showIcon.value) {
showIcon.value = true;
}
});
const { label, icon } = options.bar.windowtitle;
label.connect('changed', () => {
if (!label.value && !icon.value) {
icon.value = true;
}
});
icon.connect('changed', () => {
if (!label.value && !icon.value) {
label.value = true;
}
});

View File

@@ -1,133 +0,0 @@
const battery = await Service.import('battery');
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { label: show_label, rightClick, middleClick, scrollUp, scrollDown, hideLabelWhenFull } = options.bar.battery;
const BatteryLabel = (): BarBoxChild => {
const isVis = Variable(battery.available);
const batIcon = Utils.merge(
[battery.bind('percent'), battery.bind('charging'), battery.bind('charged')],
(batPercent: number, batCharging, batCharged) => {
if (batCharged) return `battery-level-100-charged-symbolic`;
else return `battery-level-${Math.floor(batPercent / 10) * 10}${batCharging ? '-charging' : ''}-symbolic`;
},
);
battery.connect('changed', ({ available }) => {
isVis.value = available;
});
const formatTime = (seconds: number): Record<string, number> => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return { hours, minutes };
};
const generateTooltip = (timeSeconds: number, isCharging: boolean, isCharged: boolean): string => {
if (isCharged) {
return 'Full';
}
const { hours, minutes } = formatTime(timeSeconds);
if (isCharging) {
return `Time to full: ${hours} h ${minutes} min`;
} else {
return `Time to empty: ${hours} h ${minutes} min`;
}
};
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `battery-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
visible: battery.bind('available'),
tooltip_text: battery.bind('time_remaining').as((t) => t.toString()),
children: Utils.merge(
[
battery.bind('available'),
show_label.bind('value'),
battery.bind('charged'),
hideLabelWhenFull.bind('value'),
],
(batAvail, showLabel, isCharged, hideWhenFull) => {
if (batAvail && showLabel) {
return [
Widget.Icon({
class_name: 'bar-button-icon battery',
icon: batIcon,
}),
...(hideWhenFull && isCharged
? []
: [
Widget.Label({
class_name: 'bar-button-label battery',
label: battery.bind('percent').as((p) => `${Math.floor(p)}%`),
}),
]),
];
} else if (batAvail && !showLabel) {
return [
Widget.Icon({
class_name: 'bar-button-icon battery',
icon: batIcon,
}),
];
} else {
return [];
}
},
),
setup: (self) => {
self.hook(battery, () => {
if (battery.available) {
self.tooltip_text = generateTooltip(battery.time_remaining, battery.charging, battery.charged);
}
});
},
}),
isVis,
boxClass: 'battery',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
onPrimaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'energymenu');
},
},
};
};
export { BatteryLabel };

View File

@@ -1,74 +0,0 @@
const bluetooth = await Service.import('bluetooth');
import Gdk from 'gi://Gdk?version=3.0';
import options from 'options';
import { openMenu } from '../utils.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { label, rightClick, middleClick, scrollDown, scrollUp } = options.bar.bluetooth;
const Bluetooth = (): BarBoxChild => {
const btIcon = Widget.Label({
label: bluetooth.bind('enabled').as((v) => (v ? '󰂯' : '󰂲')),
class_name: 'bar-button-icon bluetooth txt-icon bar',
});
const btText = Widget.Label({
label: Utils.merge([bluetooth.bind('enabled'), bluetooth.bind('connected_devices')], (btEnabled, btDevices) => {
return btEnabled && btDevices.length ? ` Connected (${btDevices.length})` : btEnabled ? 'On' : 'Off';
}),
class_name: 'bar-button-label bluetooth',
});
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `bluetooth-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: options.bar.bluetooth.label.bind('value').as((showLabel) => {
if (showLabel) {
return [btIcon, btText];
}
return [btIcon];
}),
}),
isVisible: true,
boxClass: 'bluetooth',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'bluetoothmenu');
},
},
};
};
export { Bluetooth };

View File

@@ -1,83 +0,0 @@
import Gdk from 'gi://Gdk?version=3.0';
import GLib from 'gi://GLib';
import { openMenu } from '../utils.js';
import options from 'options';
import { DateTime } from 'types/@girs/glib-2.0/glib-2.0.cjs';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { format, icon, showIcon, showTime, rightClick, middleClick, scrollUp, scrollDown } = options.bar.clock;
const { style } = options.theme.bar.buttons;
const date = Variable(GLib.DateTime.new_now_local(), {
poll: [1000, (): DateTime => GLib.DateTime.new_now_local()],
});
const time = Utils.derive([date, format], (c, f) => c.format(f) || '');
const Clock = (): BarBoxChild => {
const clockTime = Widget.Label({
class_name: 'bar-button-label clock bar',
label: time.bind(),
});
const clockIcon = Widget.Label({
label: icon.bind('value'),
class_name: 'bar-button-icon clock txt-icon bar',
});
return {
component: Widget.Box({
className: Utils.merge(
[style.bind('value'), showIcon.bind('value'), showTime.bind('value')],
(btnStyle, shwIcn, shwLbl) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `clock-container ${styleMap[btnStyle]} ${!shwLbl ? 'no-label' : ''} ${!shwIcn ? 'no-icon' : ''}`;
},
),
children: Utils.merge([showIcon.bind('value'), showTime.bind('value')], (shIcn, shTm) => {
if (shIcn && !shTm) {
return [clockIcon];
} else if (shTm && !shIcn) {
return [clockTime];
}
return [clockIcon, clockTime];
}),
}),
isVisible: true,
boxClass: 'clock',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'calendarmenu');
},
},
};
};
export { Clock };

View File

@@ -1,89 +0,0 @@
import { MediaTags } from 'lib/types/audio.js';
import { Opt } from 'lib/option';
import { Variable } from 'types/variable';
import { MprisPlayer } from 'types/service/mpris';
const getIconForPlayer = (playerName: string): string => {
const windowTitleMap = [
['Firefox', '󰈹'],
['Microsoft Edge', '󰇩'],
['Discord', ''],
['Plex', '󰚺'],
['Spotify', '󰓇'],
['Vlc', '󰕼'],
['Mpv', ''],
['Rhythmbox', '󰓃'],
['Google Chrome', ''],
['Brave Browser', '󰖟'],
['Chromium', ''],
['Opera', ''],
['Vivaldi', '󰖟'],
['Waterfox', '󰈹'],
['Thorium', '󰈹'],
['Zen Browser', '󰈹'],
['Floorp', '󰈹'],
['(.*)', '󰝚'],
];
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0], 'i').test(playerName));
return foundMatch ? foundMatch[1] : '󰝚';
};
const isValidMediaTag = (tag: unknown): tag is keyof MediaTags => {
if (typeof tag !== 'string') {
return false;
}
const mediaTagKeys = ['title', 'artists', 'artist', 'album', 'name', 'identity'] as const;
return (mediaTagKeys as readonly string[]).includes(tag);
};
export const generateMediaLabel = (
truncation_size: Opt<number>,
show_label: Opt<boolean>,
format: Opt<string>,
songIcon: Variable<string>,
activePlayer: Variable<MprisPlayer>,
): string => {
if (activePlayer.value && show_label.value) {
const { track_title, identity, track_artists, track_album, name } = activePlayer.value;
songIcon.value = getIconForPlayer(identity);
const mediaTags: MediaTags = {
title: track_title,
artists: track_artists.join(', '),
artist: track_artists[0] || '',
album: track_album,
name: name,
identity: identity,
};
const mediaFormat = format.getValue();
const truncatedLabel = mediaFormat.replace(
/{(title|artists|artist|album|name|identity)(:[^}]*)?}/g,
(_, p1: string | undefined, p2: string | undefined) => {
if (!isValidMediaTag(p1)) {
return '';
}
const value = p1 !== undefined ? mediaTags[p1] : '';
const suffix = p2?.length ? p2.slice(1) : '';
return value ? value + suffix : '';
},
);
const maxLabelSize = truncation_size.value;
let mediaLabel = truncatedLabel;
if (maxLabelSize > 0 && truncatedLabel.length > maxLabelSize) {
mediaLabel = `${truncatedLabel.substring(0, maxLabelSize)}...`;
}
return mediaLabel.length ? mediaLabel : 'Media';
} else {
songIcon.value = getIconForPlayer(activePlayer.value?.identity || '');
return `Media`;
}
};

View File

@@ -1,83 +0,0 @@
import Gdk from 'gi://Gdk?version=3.0';
const mpris = await Service.import('mpris');
import { openMenu } from '../utils.js';
import options from 'options';
import { getCurrentPlayer } from 'lib/shared/media.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand } from 'customModules/utils.js';
import { generateMediaLabel } from './helpers.js';
const { truncation, truncation_size, show_label, show_active_only, rightClick, middleClick, format } =
options.bar.media;
const Media = (): BarBoxChild => {
const activePlayer = Variable(mpris.players[0]);
const isVis = Variable(!show_active_only.value);
show_active_only.connect('changed', () => {
isVis.value = !show_active_only.value || mpris.players.length > 0;
});
mpris.connect('changed', () => {
const curPlayer = getCurrentPlayer(activePlayer.value);
activePlayer.value = curPlayer;
isVis.value = !show_active_only.value || mpris.players.length > 0;
});
const songIcon = Variable('');
const mediaLabel = Utils.watch('Media', [mpris, truncation, truncation_size, show_label, format], () => {
return generateMediaLabel(truncation_size, show_label, format, songIcon, activePlayer);
});
return {
component: Widget.Box({
visible: false,
child: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
(style) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `media-container ${styleMap[style]}`;
},
),
child: Widget.Box({
children: [
Widget.Label({
class_name: 'bar-button-icon media txt-icon bar',
label: songIcon.bind('value').as((v) => v || '󰝚'),
}),
Widget.Label({
class_name: 'bar-button-label media',
label: mediaLabel,
}),
],
}),
}),
}),
isVis,
boxClass: 'media',
props: {
on_scroll_up: () => activePlayer.value?.next(),
on_scroll_down: () => activePlayer.value?.previous(),
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'mediamenu');
},
onSecondaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
},
onMiddleClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
},
},
};
};
export { Media };

View File

@@ -1,59 +0,0 @@
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
import Gdk from 'gi://Gdk?version=3.0';
import { BarBoxChild } from 'lib/types/bar.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options';
import Button from 'types/widgets/button.js';
import { openMenu } from '../utils.js';
import { getDistroIcon } from 'lib/utils.js';
const { rightClick, middleClick, scrollUp, scrollDown, autoDetectIcon, icon } = options.bar.launcher;
const Menu = (): BarBoxChild => {
return {
component: Widget.Box({
className: Utils.merge([options.theme.bar.buttons.style.bind('value')], (style) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `dashboard ${styleMap[style]}`;
}),
child: Widget.Label({
class_name: 'bar-menu_label bar-button_icon txt-icon bar',
label: Utils.merge([autoDetectIcon.bind('value'), icon.bind('value')], (autoDetect, icon): string => {
return autoDetect ? getDistroIcon() : icon;
}),
}),
}),
isVisible: true,
boxClass: 'dashboard',
props: {
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'dashboardmenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Menu };

View File

@@ -1,122 +0,0 @@
import Gdk from 'gi://Gdk?version=3.0';
const network = await Service.import('network');
import options from 'options';
import { openMenu } from '../utils.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
import { Wifi } from 'types/service/network.js';
const formatFrequency = (frequency: number): string => {
return `${(frequency / 1000).toFixed(2)}MHz`;
};
const formatWifiInfo = (wifi: Wifi): string => {
const netSsid = wifi.ssid === '' ? 'None' : wifi.ssid;
const wifiStrength = wifi.strength >= 0 ? wifi.strength : '--';
const wifiFreq = wifi.frequency >= 0 ? formatFrequency(wifi.frequency) : '--';
return `Network: ${netSsid} \nSignal Strength: ${wifiStrength}% \nFrequency: ${wifiFreq}`;
};
const {
label: networkLabel,
truncation,
truncation_size,
rightClick,
middleClick,
scrollDown,
scrollUp,
showWifiInfo,
} = options.bar.network;
const Network = (): BarBoxChild => {
return {
component: Widget.Box({
vpack: 'fill',
vexpand: true,
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), networkLabel.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `network-container ${styleMap[style]}${!showLabel ? ' no-label' : ''}`;
},
),
children: [
Widget.Icon({
class_name: 'bar-button-icon network-icon',
icon: Utils.merge(
[network.bind('primary'), network.bind('wifi'), network.bind('wired')],
(pmry, wfi, wrd) => {
if (pmry === 'wired') {
return wrd.icon_name;
}
return wfi.icon_name;
},
),
}),
Widget.Box({
child: Utils.merge(
[
network.bind('primary'),
network.bind('wifi'),
networkLabel.bind('value'),
truncation.bind('value'),
truncation_size.bind('value'),
showWifiInfo.bind('value'),
],
(pmry, wfi, showLbl, trunc, tSize, showWfiInfo) => {
if (!showLbl) {
return Widget.Box();
}
if (pmry === 'wired') {
return Widget.Label({
class_name: 'bar-button-label network-label',
label: 'Wired'.substring(0, tSize),
});
}
return Widget.Label({
class_name: 'bar-button-label network-label',
label: wfi.ssid ? `${trunc ? wfi.ssid.substring(0, tSize) : wfi.ssid}` : '--',
tooltipText: showWfiInfo ? formatWifiInfo(wfi) : '',
});
},
),
}),
],
}),
isVisible: true,
boxClass: 'network',
props: {
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'networkmenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Network };

View File

@@ -1,94 +0,0 @@
import Gdk from 'gi://Gdk?version=3.0';
import { openMenu } from '../utils.js';
import options from 'options';
import { filterNotifications } from 'lib/shared/notifications.js';
import { BarBoxChild } from 'lib/types/bar.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { show_total, rightClick, middleClick, scrollUp, scrollDown, hideCountWhenZero } = options.bar.notifications;
const { ignore } = options.notifications;
const notifs = await Service.import('notifications');
export const Notifications = (): BarBoxChild => {
return {
component: Widget.Box({
hpack: 'start',
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), show_total.bind('value')],
(style, showTotal) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `notifications-container ${styleMap[style]} ${!showTotal ? 'no-label' : ''}`;
},
),
child: Widget.Box({
hpack: 'start',
class_name: 'bar-notifications',
children: Utils.merge(
[
notifs.bind('notifications'),
notifs.bind('dnd'),
show_total.bind('value'),
ignore.bind('value'),
hideCountWhenZero.bind('value'),
],
(notif, dnd, showTotal, ignoredNotifs, hideCountForZero) => {
const filteredNotifications = filterNotifications(notif, ignoredNotifs);
const notifIcon = Widget.Label({
hpack: 'center',
class_name: 'bar-button-icon notifications txt-icon bar',
label: dnd ? '󰂛' : filteredNotifications.length > 0 ? '󱅫' : '󰂚',
});
const notifLabel = Widget.Label({
hpack: 'center',
class_name: 'bar-button-label notifications',
label: filteredNotifications.length.toString(),
});
if (showTotal) {
if (hideCountForZero && filteredNotifications.length === 0) {
return [notifIcon];
}
return [notifIcon, notifLabel];
}
return [notifIcon];
},
),
}),
}),
isVisible: true,
boxClass: 'notifications',
props: {
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'notificationsmenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};

View File

@@ -1,67 +0,0 @@
import Gdk from 'gi://Gdk?version=3.0';
import { BarBoxChild, SelfButton } from 'lib/types/bar';
import { Notify } from 'lib/utils';
const systemtray = await Service.import('systemtray');
import options from 'options';
const { ignore, customIcons } = options.bar.systray;
const SysTray = (): BarBoxChild => {
const isVis = Variable(false);
const items = Utils.merge(
[systemtray.bind('items'), ignore.bind('value'), customIcons.bind('value')],
(items, ignored, custIcons) => {
const filteredTray = items.filter(({ id }) => !ignored.includes(id) && id !== null);
isVis.value = filteredTray.length > 0;
return filteredTray.map((item) => {
const matchedCustomIcon = Object.keys(custIcons).find((iconRegex) => item.id.match(iconRegex));
if (matchedCustomIcon !== undefined) {
const iconLabel = custIcons[matchedCustomIcon].icon || '󰠫';
const iconColor = custIcons[matchedCustomIcon].color;
return Widget.Button({
cursor: 'pointer',
child: Widget.Label({
class_name: 'systray-icon txt-icon',
label: iconLabel,
css: iconColor ? `color: ${iconColor}` : '',
}),
on_primary_click: (_: SelfButton, event: Gdk.Event) => item.activate(event),
on_secondary_click: (_, event) => item.openMenu(event),
onMiddleClick: () => Notify({ summary: 'App Name', body: item.id }),
tooltip_markup: item.bind('tooltip_markup'),
});
}
return Widget.Button({
cursor: 'pointer',
child: Widget.Icon({
class_name: 'systray-icon',
icon: item.bind('icon'),
}),
on_primary_click: (_: SelfButton, event: Gdk.Event) => item.activate(event),
on_secondary_click: (_, event) => item.openMenu(event),
onMiddleClick: () => Notify({ summary: 'App Name', body: item.id }),
tooltip_markup: item.bind('tooltip_markup'),
});
});
},
);
return {
component: Widget.Box({
class_name: 'systray-container',
children: items,
}),
isVisible: true,
boxClass: 'systray',
isVis,
props: {},
};
};
export { SysTray };

View File

@@ -1,107 +0,0 @@
import Gdk from 'gi://Gdk?version=3.0';
const audio = await Service.import('audio');
import { openMenu } from '../utils.js';
import options from 'options';
import { Binding } from 'lib/utils.js';
import { VolumeIcons } from 'lib/types/volume.js';
import { BarBoxChild } from 'lib/types/bar.js';
import { Bind } from 'lib/types/variable.js';
import Button from 'types/widgets/button.js';
import { Attribute, Child } from 'lib/types/widget.js';
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
const { rightClick, middleClick, scrollUp, scrollDown } = options.bar.volume;
const Volume = (): BarBoxChild => {
const icons: VolumeIcons = {
101: '󰕾',
66: '󰕾',
34: '󰖀',
1: '󰕿',
0: '󰝟',
};
const getIcon = (): Bind => {
const icon: Binding<number> = Utils.merge(
[audio.speaker.bind('is_muted'), audio.speaker.bind('volume')],
(isMuted, vol) => {
if (isMuted) return 0;
const foundVol = [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100);
if (foundVol !== undefined) {
return foundVol;
}
return 101;
},
);
return icon.as((i: number) => (i !== undefined ? icons[i] : icons[101]));
};
const volIcn = Widget.Label({
label: getIcon(),
class_name: 'bar-button-icon volume txt-icon bar',
});
const volPct = Widget.Label({
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
class_name: 'bar-button-label volume',
});
return {
component: Widget.Box({
vexpand: true,
tooltip_text: Utils.merge(
[audio.speaker.bind('description'), getIcon()],
(desc, icon) => ` ${icon} ${desc}`,
),
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), options.bar.volume.label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `volume-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: options.bar.volume.label.bind('value').as((showLabel) => {
if (showLabel) {
return [volIcn, volPct];
}
return [volIcn];
}),
}),
isVisible: true,
boxClass: 'volume',
props: {
onPrimaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
openMenu(clicked, event, 'audiomenu');
},
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { Volume };

View File

@@ -1,242 +0,0 @@
const hyprland = await Service.import('hyprland');
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils';
import { BarBoxChild } from 'lib/types/bar';
import { Attribute, Child } from 'lib/types/widget';
import { capitalizeFirstLetter } from 'lib/utils';
import options from 'options';
import Gdk from 'types/@girs/gdk-3.0/gdk-3.0';
import { ActiveClient } from 'types/service/hyprland';
import Button from 'types/widgets/button';
import Label from 'types/widgets/label';
const { leftClick, rightClick, middleClick, scrollDown, scrollUp } = options.bar.windowtitle;
const filterTitle = (windowtitle: ActiveClient): Record<string, string> => {
const windowTitleMap = [
// user provided values
...options.bar.windowtitle.title_map.value,
// Original Entries
['kitty', '󰄛', 'Kitty Terminal'],
['firefox', '󰈹', 'Firefox'],
['microsoft-edge', '󰇩', 'Edge'],
['discord', '', 'Discord'],
['vesktop', '', 'Vesktop'],
['org.kde.dolphin', '', 'Dolphin'],
['plex', '󰚺', 'Plex'],
['steam', '', 'Steam'],
['spotify', '󰓇', 'Spotify'],
['ristretto', '󰋩', 'Ristretto'],
['obsidian', '󱓧', 'Obsidian'],
// Browsers
['google-chrome', '', 'Google Chrome'],
['brave-browser', '󰖟', 'Brave Browser'],
['chromium', '', 'Chromium'],
['opera', '', 'Opera'],
['vivaldi', '󰖟', 'Vivaldi'],
['waterfox', '󰖟', 'Waterfox'],
['thorium', '󰖟', 'Waterfox'],
['tor-browser', '', 'Tor Browser'],
['floorp', '󰈹', 'Floorp'],
// Terminals
['gnome-terminal', '', 'GNOME Terminal'],
['konsole', '', 'Konsole'],
['alacritty', '', 'Alacritty'],
['wezterm', '', 'Wezterm'],
['foot', '󰽒', 'Foot Terminal'],
['tilix', '', 'Tilix'],
['xterm', '', 'XTerm'],
['urxvt', '', 'URxvt'],
['st', '', 'st Terminal'],
// Development Tools
['code', '󰨞', 'Visual Studio Code'],
['vscode', '󰨞', 'VS Code'],
['sublime-text', '', 'Sublime Text'],
['atom', '', 'Atom'],
['android-studio', '󰀴', 'Android Studio'],
['intellij-idea', '', 'IntelliJ IDEA'],
['pycharm', '󱃖', 'PyCharm'],
['webstorm', '󱃖', 'WebStorm'],
['phpstorm', '󱃖', 'PhpStorm'],
['eclipse', '', 'Eclipse'],
['netbeans', '', 'NetBeans'],
['docker', '', 'Docker'],
['vim', '', 'Vim'],
['neovim', '', 'Neovim'],
['neovide', '', 'Neovide'],
['emacs', '', 'Emacs'],
// Communication Tools
['slack', '󰒱', 'Slack'],
['telegram-desktop', '', 'Telegram'],
['org.telegram.desktop', '', 'Telegram'],
['whatsapp', '󰖣', 'WhatsApp'],
['teams', '󰊻', 'Microsoft Teams'],
['skype', '󰒯', 'Skype'],
['thunderbird', '', 'Thunderbird'],
// File Managers
['nautilus', '󰝰', 'Files (Nautilus)'],
['thunar', '󰝰', 'Thunar'],
['pcmanfm', '󰝰', 'PCManFM'],
['nemo', '󰝰', 'Nemo'],
['ranger', '󰝰', 'Ranger'],
['doublecmd', '󰝰', 'Double Commander'],
['krusader', '󰝰', 'Krusader'],
// Media Players
['vlc', '󰕼', 'VLC Media Player'],
['mpv', '', 'MPV'],
['rhythmbox', '󰓃', 'Rhythmbox'],
// Graphics Tools
['gimp', '', 'GIMP'],
['inkscape', '', 'Inkscape'],
['krita', '', 'Krita'],
['blender', '󰂫', 'Blender'],
// Video Editing
['kdenlive', '', 'Kdenlive'],
// Games and Gaming Platforms
['lutris', '󰺵', 'Lutris'],
['heroic', '󰺵', 'Heroic Games Launcher'],
['minecraft', '󰍳', 'Minecraft'],
['csgo', '󰺵', 'CS:GO'],
['dota2', '󰺵', 'Dota 2'],
// Office and Productivity
['evernote', '', 'Evernote'],
['sioyek', '', 'Sioyek'],
// Cloud Services and Sync
['dropbox', '󰇣', 'Dropbox'],
// Desktop
['^$', '󰇄', 'Desktop'],
// Fallback icon
['(.+)', '󰣆', `${capitalizeFirstLetter(windowtitle.class)}`],
];
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(windowtitle.class.toLowerCase()));
// return the default icon if no match is found or
// if the array element matched is not of size 3
if (!foundMatch || foundMatch.length !== 3) {
return {
icon: windowTitleMap[windowTitleMap.length - 1][1],
label: windowTitleMap[windowTitleMap.length - 1][2],
};
}
return {
icon: foundMatch[1],
label: foundMatch[2],
};
};
const getTitle = (client: ActiveClient, useCustomTitle: boolean, useClassName: boolean): string => {
if (useCustomTitle) return filterTitle(client).label;
if (useClassName) return client.class;
const title = client.title;
// If the title is empty or only filled with spaces, fallback to the class name
if (title.length === 0 || title.match(/^ *$/)) {
return client.class;
}
return title;
};
const truncateTitle = (title: string, max_size: number): string => {
if (max_size > 0 && title.length > max_size) {
return title.substring(0, max_size).trim() + '...';
}
return title;
};
const ClientTitle = (): BarBoxChild => {
const { custom_title, class_name, label, icon, truncation, truncation_size } = options.bar.windowtitle;
return {
component: Widget.Box({
className: Utils.merge(
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
(style, showLabel) => {
const styleMap = {
default: 'style1',
split: 'style2',
wave: 'style3',
wave2: 'style3',
};
return `windowtitle-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
},
),
children: Utils.merge(
[
hyprland.active.bind('client'),
custom_title.bind('value'),
class_name.bind('value'),
label.bind('value'),
icon.bind('value'),
truncation.bind('value'),
truncation_size.bind('value'),
],
(client, useCustomTitle, useClassName, showLabel, showIcon, truncate, truncationSize) => {
const children: Label<Child>[] = [];
if (showIcon) {
children.push(
Widget.Label({
class_name: 'bar-button-icon windowtitle txt-icon bar',
label: filterTitle(client).icon,
}),
);
}
if (showLabel) {
children.push(
Widget.Label({
class_name: `bar-button-label windowtitle ${showIcon ? '' : 'no-icon'}`,
label: truncateTitle(
getTitle(client, useCustomTitle, useClassName),
truncate ? truncationSize : -1,
),
}),
);
}
return children;
},
),
}),
isVisible: true,
boxClass: 'windowtitle',
props: {
setup: (self: Button<Child, Attribute>): void => {
self.hook(options.bar.scrollSpeed, () => {
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
self.on_primary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(leftClick.value, { clicked, event });
};
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(rightClick.value, { clicked, event });
};
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
runAsyncCommand(middleClick.value, { clicked, event });
};
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollUp.value, { clicked, event });
};
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
throttledHandler(scrollDown.value, { clicked, event });
};
});
},
},
};
};
export { ClientTitle };

View File

@@ -1,162 +0,0 @@
const hyprland = await Service.import('hyprland');
import { MonitorMap, WorkspaceMap, WorkspaceRule } from 'lib/types/workspace';
import options from 'options';
import { Variable } from 'types/variable';
const { workspaces, reverse_scroll, ignored } = options.bar.workspaces;
export const getWorkspacesForMonitor = (curWs: number, wsRules: WorkspaceMap, monitor: number): boolean => {
if (!wsRules || !Object.keys(wsRules).length) {
return true;
}
const monitorMap: MonitorMap = {};
const workspaceMonitorList = hyprland?.workspaces?.map((m) => ({ id: m.monitorID, name: m.monitor }));
const monitors = [
...new Map([...workspaceMonitorList, ...hyprland.monitors].map((item) => [item.id, item])).values(),
];
monitors.forEach((m) => (monitorMap[m.id] = m.name));
const currentMonitorName = monitorMap[monitor];
const monitorWSRules = wsRules[currentMonitorName];
if (monitorWSRules === undefined) {
return true;
}
return monitorWSRules.includes(curWs);
};
export const getWorkspaceRules = (): WorkspaceMap => {
try {
const rules = Utils.exec('hyprctl workspacerules -j');
const workspaceRules: WorkspaceMap = {};
JSON.parse(rules).forEach((rule: WorkspaceRule) => {
const workspaceNum = parseInt(rule.workspaceString, 10);
if (isNaN(workspaceNum)) {
return;
}
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
workspaceRules[rule.monitor].push(workspaceNum);
} else {
workspaceRules[rule.monitor] = [workspaceNum];
}
});
return workspaceRules;
} catch (err) {
console.error(err);
return {};
}
};
export const getCurrentMonitorWorkspaces = (monitor: number): number[] => {
if (hyprland.monitors.length === 1) {
return Array.from({ length: workspaces.value }, (_, i) => i + 1);
}
const monitorWorkspaces = getWorkspaceRules();
const monitorMap: MonitorMap = {};
hyprland.monitors.forEach((m) => (monitorMap[m.id] = m.name));
const currentMonitorName = monitorMap[monitor];
return monitorWorkspaces[currentMonitorName];
};
type ThrottledScrollHandlers = {
throttledScrollUp: () => void;
throttledScrollDown: () => void;
};
export const isWorkspaceIgnored = (ignoredWorkspaces: Variable<string>, workspaceNumber: number): boolean => {
if (ignoredWorkspaces.value === '') return false;
const ignoredWsRegex = new RegExp(ignoredWorkspaces.value);
return ignoredWsRegex.test(workspaceNumber.toString());
};
const navigateWorkspace = (
direction: 'next' | 'prev',
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable<string>,
): void => {
const workspacesList = activeWorkspaces
? hyprland.workspaces.filter((ws) => hyprland.active.monitor.id === ws.monitorID).map((ws) => ws.id)
: currentMonitorWorkspaces.value || Array.from({ length: workspaces.value }, (_, i) => i + 1);
if (workspacesList.length === 0) return;
const currentIndex = workspacesList.indexOf(hyprland.active.workspace.id);
const step = direction === 'next' ? 1 : -1;
let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length;
let attempts = 0;
while (attempts < workspacesList.length) {
const targetWS = workspacesList[newIndex];
if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) {
hyprland.messageAsync(`dispatch workspace ${targetWS}`);
return;
}
newIndex = (newIndex + step + workspacesList.length) % workspacesList.length;
attempts++;
}
};
export const goToNextWS = (
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable<string>,
): void => {
navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
};
export const goToPrevWS = (
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean,
ignoredWorkspaces: Variable<string>,
): void => {
navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
};
export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T {
let inThrottle: boolean;
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
} as T;
}
export const createThrottledScrollHandlers = (
scrollSpeed: number,
currentMonitorWorkspaces: Variable<number[]>,
activeWorkspaces: boolean = false,
): ThrottledScrollHandlers => {
const throttledScrollUp = throttle(() => {
if (reverse_scroll.value) {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
} else {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
}
}, 200 / scrollSpeed);
const throttledScrollDown = throttle(() => {
if (reverse_scroll.value) {
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
} else {
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
}
}, 200 / scrollSpeed);
return { throttledScrollUp, throttledScrollDown };
};

View File

@@ -1,44 +0,0 @@
import options from 'options';
import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers';
import { BarBoxChild, SelfButton } from 'lib/types/bar';
import { occupiedWses } from './variants/occupied';
import { defaultWses } from './variants/default';
const { workspaces, scroll_speed } = options.bar.workspaces;
const Workspaces = (monitor = -1): BarBoxChild => {
const currentMonitorWorkspaces = Variable(getCurrentMonitorWorkspaces(monitor));
workspaces.connect('changed', () => {
currentMonitorWorkspaces.value = getCurrentMonitorWorkspaces(monitor);
});
return {
component: Widget.Box({
class_name: 'workspaces-box-container',
child: options.bar.workspaces.hideUnoccupied.bind('value').as((hideUnoccupied) => {
return hideUnoccupied ? occupiedWses(monitor) : defaultWses(monitor);
}),
}),
isVisible: true,
boxClass: 'workspaces',
props: {
setup: (self: SelfButton): void => {
Utils.merge(
[scroll_speed.bind('value'), options.bar.workspaces.hideUnoccupied.bind('value')],
(scroll_speed, hideUnoccupied) => {
const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers(
scroll_speed,
currentMonitorWorkspaces,
hideUnoccupied,
);
self.on_scroll_up = throttledScrollUp;
self.on_scroll_down = throttledScrollDown;
},
);
},
},
};
};
export { Workspaces };

View File

@@ -1,199 +0,0 @@
import { defaultApplicationIcons } from 'lib/constants/workspaces';
import type { ClientAttributes, AppIconOptions, WorkspaceIconMap } from 'lib/types/workspace';
import { isValidGjsColor } from 'lib/utils';
import options from 'options';
import { Monitor } from 'types/service/hyprland';
const hyprland = await Service.import('hyprland');
const { monochrome, background } = options.theme.bar.buttons;
const { background: wsBackground, active } = options.theme.bar.buttons.workspaces;
const { showWsIcons, showAllActive, numbered_active_indicator: activeIndicator } = options.bar.workspaces;
const isWorkspaceActiveOnMonitor = (monitor: number, monitors: Monitor[], i: number): boolean => {
return showAllActive.value && monitors[monitor]?.activeWorkspace?.id === i;
};
const getWsIcon = (wsIconMap: WorkspaceIconMap, i: number): string => {
const iconEntry = wsIconMap[i];
if (!iconEntry) {
return `${i}`;
}
const hasIcon = typeof iconEntry === 'object' && 'icon' in iconEntry && iconEntry.icon !== '';
if (typeof iconEntry === 'string' && iconEntry !== '') {
return iconEntry;
}
if (hasIcon) {
return iconEntry.icon;
}
return `${i}`;
};
export const getWsColor = (
wsIconMap: WorkspaceIconMap,
i: number,
smartHighlight: boolean,
monitor: number,
monitors: Monitor[],
): string => {
const iconEntry = wsIconMap[i];
const hasColor = typeof iconEntry === 'object' && 'color' in iconEntry && isValidGjsColor(iconEntry.color);
if (!iconEntry) {
return '';
}
if (
showWsIcons.value &&
smartHighlight &&
activeIndicator.value === 'highlight' &&
(hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i))
) {
const iconColor = monochrome.value ? background : wsBackground;
const iconBackground = hasColor && isValidGjsColor(iconEntry.color) ? iconEntry.color : active.value;
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 '';
};
export const getAppIcon = (
workspaceIndex: number,
removeDuplicateIcons: boolean,
{ iconMap: userDefinedIconMap, defaultIcon, emptyIcon }: AppIconOptions,
): string => {
// append the default icons so user defined icons take precedence
const iconMap = { ...userDefinedIconMap, ...defaultApplicationIcons };
// detect the clients attributes on the current workspace
const clients: ReadonlyArray<ClientAttributes> = hyprland.clients
.filter((c) => c.workspace.id === workspaceIndex)
.map((c) => [c.class, c.title]);
if (!clients.length) {
return emptyIcon;
}
// map the client attributes to icons
let icons = clients
.map(([clientClass, clientTitle]) => {
const maybeIcon = Object.entries(iconMap).find(([matcher]) => {
// non-valid Regex construction could result in a syntax error
try {
if (matcher.startsWith('class:')) {
const re = matcher.substring(6);
return new RegExp(re).test(clientClass);
}
if (matcher.startsWith('title:')) {
const re = matcher.substring(6);
return new RegExp(re).test(clientTitle);
}
return new RegExp(matcher, 'i').test(clientClass);
} catch {
return false;
}
});
if (!maybeIcon) {
return undefined;
}
return maybeIcon.at(1);
})
.filter((x) => x);
// remove duplicate icons
if (removeDuplicateIcons) {
icons = [...new Set(icons)];
}
if (icons.length) {
return icons.join(' ');
}
return defaultIcon;
};
export const renderClassnames = (
showIcons: boolean,
showNumbered: boolean,
numberedActiveIndicator: string,
showWsIcons: boolean,
smartHighlight: boolean,
monitor: number,
monitors: Monitor[],
i: number,
): string => {
if (showIcons) {
return 'workspace-icon txt-icon bar';
}
if (showNumbered || showWsIcons) {
const numActiveInd =
hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)
? numberedActiveIndicator
: '';
const wsIconClass = showWsIcons ? 'txt-icon' : '';
const smartHighlightClass = smartHighlight ? 'smart-highlight' : '';
const className = `workspace-number can_${numberedActiveIndicator} ${numActiveInd} ${wsIconClass} ${smartHighlightClass}`;
return className.trim();
}
return 'default';
};
export const renderLabel = (
showIcons: boolean,
available: string,
active: string,
occupied: string,
showAppIcons: boolean,
appIcons: string,
workspaceMask: boolean,
showWsIcons: boolean,
wsIconMap: WorkspaceIconMap,
i: number,
index: number,
monitor: number,
monitors: Monitor[],
): string => {
if (showAppIcons) {
return appIcons;
}
if (showIcons) {
if (hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)) {
return active;
}
if ((hyprland.getWorkspace(i)?.windows || 0) > 0) {
return occupied;
}
if (monitor !== -1) {
return available;
}
}
if (showWsIcons) {
return getWsIcon(wsIconMap, i);
}
return workspaceMask ? `${index + 1}` : `${i}`;
};

View File

@@ -1,161 +0,0 @@
const hyprland = await Service.import('hyprland');
import options from 'options';
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
import { range } from 'lib/utils';
import { BoxWidget } from 'lib/types/widget';
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from '../utils';
import { WorkspaceIconMap } from 'lib/types/workspace';
import { Monitor } from 'types/service/hyprland';
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored } = options.bar.workspaces;
export const defaultWses = (monitor: number): BoxWidget => {
return Widget.Box({
children: Utils.merge(
[workspaces.bind('value'), monitorSpecific.bind('value'), ignored.bind('value')],
(workspaces: number, monitorSpecific: boolean) => {
return range(workspaces || 8)
.filter((workspaceNumber) => {
if (!monitorSpecific) {
return true;
}
const workspaceRules = getWorkspaceRules();
return (
getWorkspacesForMonitor(workspaceNumber, workspaceRules, monitor) &&
!isWorkspaceIgnored(ignored, workspaceNumber)
);
})
.sort((a, b) => {
return a - b;
})
.map((i, index) => {
return Widget.Button({
class_name: 'workspace-button',
on_primary_click: () => {
hyprland.messageAsync(`dispatch workspace ${i}`);
},
child: Widget.Label({
attribute: i,
vpack: 'center',
css: Utils.merge(
[
spacing.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.theme.matugen.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
],
(
sp: number,
showWsIcons: boolean,
workspaceIconMap: WorkspaceIconMap,
matugen: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
return (
`margin: 0rem ${0.375 * sp}rem;` +
`${showWsIcons && !matugen ? getWsColor(workspaceIconMap, i, smartHighlight, monitor, monitors) : ''}`
);
},
),
class_name: Utils.merge(
[
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.show_numbered.bind('value'),
options.bar.workspaces.numbered_active_indicator.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
],
(
showIcons: boolean,
showNumbered: boolean,
numberedActiveIndicator: string,
showWsIcons: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
return renderClassnames(
showIcons,
showNumbered,
numberedActiveIndicator,
showWsIcons,
smartHighlight,
monitor,
monitors,
i,
);
},
),
label: Utils.merge(
[
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
options.bar.workspaces.icons.occupied.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.bar.workspaces.showApplicationIcons.bind('value'),
options.bar.workspaces.applicationIconOncePerWorkspace.bind('value'),
options.bar.workspaces.applicationIconMap.bind('value'),
options.bar.workspaces.applicationIconEmptyWorkspace.bind('value'),
options.bar.workspaces.applicationIconFallback.bind('value'),
workspaceMask.bind('value'),
hyprland.bind('monitors'),
],
(
showIcons: boolean,
available: string,
active: string,
occupied: string,
wsIconMap: WorkspaceIconMap,
showWsIcons: boolean,
showAppIcons,
applicationIconOncePerWorkspace,
applicationIconMap,
applicationIconEmptyWorkspace,
applicationIconFallback,
workspaceMask: boolean,
monitors: Monitor[],
) => {
const appIcons = showAppIcons
? getAppIcon(i, applicationIconOncePerWorkspace, {
iconMap: applicationIconMap,
defaultIcon: applicationIconFallback,
emptyIcon: applicationIconEmptyWorkspace,
})
: '';
return renderLabel(
showIcons,
available,
active,
occupied,
showAppIcons,
appIcons,
workspaceMask,
showWsIcons,
wsIconMap,
i,
index,
monitor,
monitors,
);
},
),
setup: (self) => {
self.hook(hyprland, () => {
self.toggleClassName('active', hyprland.active.workspace.id === i);
self.toggleClassName('occupied', (hyprland.getWorkspace(i)?.windows || 0) > 0);
});
},
}),
});
});
},
),
});
};

View File

@@ -1,165 +0,0 @@
const hyprland = await Service.import('hyprland');
import options from 'options';
import { getWorkspaceRules, getWorkspacesForMonitor, isWorkspaceIgnored } from '../helpers';
import { Monitor, Workspace } from 'types/service/hyprland';
import { getAppIcon, getWsColor, renderClassnames, renderLabel } from '../utils';
import { range } from 'lib/utils';
import { BoxWidget } from 'lib/types/widget';
import { WorkspaceIconMap } from 'lib/types/workspace';
const { workspaces, monitorSpecific, workspaceMask, spacing, ignored, showAllActive } = options.bar.workspaces;
export const occupiedWses = (monitor: number): BoxWidget => {
const workspaceRules = getWorkspaceRules();
return Widget.Box({
children: Utils.merge(
[
monitorSpecific.bind('value'),
hyprland.bind('workspaces'),
workspaceMask.bind('value'),
workspaces.bind('value'),
options.bar.workspaces.show_icons.bind('value'),
options.bar.workspaces.icons.available.bind('value'),
options.bar.workspaces.icons.active.bind('value'),
options.bar.workspaces.icons.occupied.bind('value'),
options.bar.workspaces.show_numbered.bind('value'),
options.bar.workspaces.numbered_active_indicator.bind('value'),
spacing.bind('value'),
options.bar.workspaces.workspaceIconMap.bind('value'),
options.bar.workspaces.showWsIcons.bind('value'),
options.bar.workspaces.showApplicationIcons.bind('value'),
options.bar.workspaces.applicationIconOncePerWorkspace.bind('value'),
options.bar.workspaces.applicationIconMap.bind('value'),
options.bar.workspaces.applicationIconEmptyWorkspace.bind('value'),
options.bar.workspaces.applicationIconFallback.bind('value'),
options.theme.matugen.bind('value'),
options.theme.bar.buttons.workspaces.smartHighlight.bind('value'),
hyprland.bind('monitors'),
ignored.bind('value'),
showAllActive.bind('value'),
],
(
monitorSpecific: boolean,
wkSpaces: Workspace[],
workspaceMask: boolean,
totalWkspcs: number,
showIcons: boolean,
available: string,
active: string,
occupied: string,
showNumbered: boolean,
numberedActiveIndicator: string,
spacing: number,
wsIconMap: WorkspaceIconMap,
showWsIcons: boolean,
showAppIcons,
applicationIconOncePerWorkspace,
applicationIconMap,
applicationIconEmptyWorkspace,
applicationIconFallback,
matugen: boolean,
smartHighlight: boolean,
monitors: Monitor[],
) => {
const activeId = hyprland.active.workspace.id;
let allWkspcs = range(totalWkspcs || 8);
const activeWorkspaces = wkSpaces.map((w) => w.id);
// Sometimes hyprland doesn't have all the monitors in the list
// so we complement it with monitors from the workspace list
const workspaceMonitorList = hyprland?.workspaces?.map((m) => ({
id: m.monitorID,
name: m.monitor,
}));
const curMonitor =
hyprland.monitors.find((m) => m.id === monitor) ||
workspaceMonitorList.find((m) => m.id === monitor);
const workspacesWithRules = Object.keys(workspaceRules).reduce((acc: number[], k: string) => {
return [...acc, ...workspaceRules[k]];
}, []);
const activesForMonitor = activeWorkspaces.filter((w) => {
if (
curMonitor &&
Object.hasOwnProperty.call(workspaceRules, curMonitor.name) &&
workspacesWithRules.includes(w)
) {
return workspaceRules[curMonitor.name].includes(w);
}
return true;
});
if (monitorSpecific) {
const wrkspcsInRange = range(totalWkspcs).filter((w) => {
return getWorkspacesForMonitor(w, workspaceRules, monitor);
});
allWkspcs = [...new Set([...activesForMonitor, ...wrkspcsInRange])];
} else {
allWkspcs = [...new Set([...allWkspcs, ...activeWorkspaces])];
}
const returnWs = allWkspcs
.sort((a, b) => {
return a - b;
})
.map((i, index) => {
if (isWorkspaceIgnored(ignored, i)) {
return Widget.Box();
}
const appIcons = showAppIcons
? getAppIcon(i, applicationIconOncePerWorkspace, {
iconMap: applicationIconMap,
defaultIcon: applicationIconFallback,
emptyIcon: applicationIconEmptyWorkspace,
})
: '';
return Widget.Button({
class_name: 'workspace-button',
on_primary_click: () => {
hyprland.messageAsync(`dispatch workspace ${i}`);
},
child: Widget.Label({
attribute: i,
vpack: 'center',
css:
`margin: 0rem ${0.375 * spacing}rem;` +
`${showWsIcons && !matugen ? getWsColor(wsIconMap, i, smartHighlight, monitor, monitors) : ''}`,
class_name: renderClassnames(
showIcons,
showNumbered,
numberedActiveIndicator,
showWsIcons,
smartHighlight,
monitor,
monitors,
i,
),
label: renderLabel(
showIcons,
available,
active,
occupied,
showAppIcons,
appIcons,
workspaceMask,
showWsIcons,
wsIconMap,
i,
index,
monitor,
monitors,
),
setup: (self) => {
self.toggleClassName('active', activeId === i);
self.toggleClassName('occupied', (hyprland.getWorkspace(i)?.windows || 0) > 0);
},
}),
});
});
return returnWs;
},
),
});
};

View File

@@ -1,75 +0,0 @@
const audio = await Service.import('audio');
import { getIcon } from '../utils.js';
import Box from 'types/widgets/box.js';
import { Attribute, Child } from 'lib/types/widget.js';
const renderActiveInput = (): Box<Child, Attribute>[] => {
return [
Widget.Box({
class_name: 'menu-slider-container input',
children: [
Widget.Button({
vexpand: false,
vpack: 'end',
setup: (self) => {
const updateClass = (): void => {
const mic = audio.microphone;
const className = `menu-active-button input ${mic.is_muted ? 'muted' : ''}`;
self.class_name = className;
};
self.hook(audio.microphone, updateClass, 'notify::is-muted');
},
on_primary_click: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
child: Widget.Icon({
class_name: 'menu-active-icon input',
setup: (self) => {
const updateIcon = (): void => {
const isMicMuted =
audio.microphone.is_muted !== null ? audio.microphone.is_muted : true;
if (audio.microphone.volume > 0) {
self.icon = getIcon(audio.microphone.volume, isMicMuted)['mic'];
} else {
self.icon = getIcon(100, true)['mic'];
}
};
self.hook(audio.microphone, updateIcon, 'notify::volume');
self.hook(audio.microphone, updateIcon, 'notify::is-muted');
},
}),
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
class_name: 'menu-active input',
hpack: 'start',
truncate: 'end',
wrap: true,
label: audio.bind('microphone').as((v) => {
return v.description === null ? 'No input device found...' : v.description;
}),
}),
Widget.Slider({
value: audio.microphone.bind('volume').as((v) => v),
class_name: 'menu-active-slider menu-slider inputs',
draw_value: false,
hexpand: true,
min: 0,
max: 1,
onChange: ({ value }) => (audio.microphone.volume = value),
}),
],
}),
Widget.Label({
class_name: 'menu-active-percentage input',
vpack: 'end',
label: audio.microphone.bind('volume').as((v) => `${Math.round(v * 100)}%`),
}),
],
}),
];
};
export { renderActiveInput };

View File

@@ -1,76 +0,0 @@
const audio = await Service.import('audio');
import { getIcon } from '../utils.js';
import Box from 'types/widgets/box.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options';
const { raiseMaximumVolume } = options.menus.volume;
const renderActivePlayback = (): Box<Child, Attribute>[] => {
return [
Widget.Box({
class_name: 'menu-slider-container playback',
children: [
Widget.Button({
vexpand: false,
vpack: 'end',
setup: (self) => {
const updateClass = (): void => {
const spkr = audio.speaker;
const className = `menu-active-button playback ${spkr.is_muted ? 'muted' : ''}`;
self.class_name = className;
};
self.hook(audio.speaker, updateClass, 'notify::is-muted');
},
on_primary_click: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
child: Widget.Icon({
class_name: 'menu-active-icon playback',
setup: (self) => {
const updateIcon = (): void => {
const isSpeakerMuted = audio.speaker.is_muted !== null ? audio.speaker.is_muted : true;
self.icon = getIcon(audio.speaker.volume, isSpeakerMuted)['spkr'];
};
self.hook(audio.speaker, updateIcon, 'notify::volume');
self.hook(audio.speaker, updateIcon, 'notify::is-muted');
},
}),
}),
Widget.Box({
vertical: true,
children: [
Widget.Label({
class_name: 'menu-active playback',
hpack: 'start',
truncate: 'end',
expand: true,
wrap: true,
label: audio.bind('speaker').as((v) => v.description || ''),
}),
Widget.Slider({
value: audio['speaker'].bind('volume'),
class_name: 'menu-active-slider menu-slider playback',
draw_value: false,
hexpand: true,
min: 0,
max: 1,
onChange: ({ value }) => (audio.speaker.volume = value),
setup: (self) => {
self.hook(raiseMaximumVolume, () => {
self.max = raiseMaximumVolume.value ? 1.5 : 1;
});
},
}),
],
}),
Widget.Label({
vpack: 'end',
class_name: 'menu-active-percentage playback',
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
}),
],
}),
];
};
export { renderActivePlayback };

View File

@@ -1,41 +0,0 @@
import { renderActiveInput } from './SelectedInput.js';
import { renderActivePlayback } from './SelectedPlayback.js';
import Box from 'types/widgets/box.js';
import { Attribute, Child } from 'lib/types/widget.js';
const activeDevices = (): Box<Child, Attribute> => {
return Widget.Box({
class_name: 'menu-section-container volume',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-label-container volume selected',
hpack: 'fill',
child: Widget.Label({
class_name: 'menu-label audio volume',
hexpand: true,
hpack: 'start',
label: 'Volume',
}),
}),
Widget.Box({
class_name: 'menu-items-section selected',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-active-container playback',
vertical: true,
children: renderActivePlayback(),
}),
Widget.Box({
class_name: 'menu-active-container input',
vertical: true,
children: renderActiveInput(),
}),
],
}),
],
});
};
export { activeDevices };

View File

@@ -1,66 +0,0 @@
const audio = await Service.import('audio');
import { InputDevices } from 'lib/types/audio';
import { Stream } from 'types/service/audio';
const renderInputDevices = (inputDevices: Stream[]): InputDevices => {
if (inputDevices.length === 0) {
return [
Widget.Button({
class_name: `menu-unfound-button input`,
child: Widget.Box({
children: [
Widget.Box({
hpack: 'start',
children: [
Widget.Label({
class_name: 'menu-button-name input',
label: 'No input devices found...',
}),
],
}),
],
}),
}),
];
}
return inputDevices.map((device) => {
return Widget.Button({
on_primary_click: () => (audio.microphone = device),
class_name: `menu-button audio input ${device}`,
child: Widget.Box({
children: [
Widget.Box({
hpack: 'start',
children: [
Widget.Label({
wrap: true,
class_name: audio.microphone
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-icon active input txt-icon'
: 'menu-button-icon input txt-icon',
),
label: '',
}),
Widget.Label({
truncate: 'end',
wrap: true,
class_name: audio.microphone
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-name active input'
: 'menu-button-name input',
),
label: device.description,
}),
],
}),
],
}),
});
});
};
export { renderInputDevices };

View File

@@ -1,60 +0,0 @@
const audio = await Service.import('audio');
import { PlaybackDevices } from 'lib/types/audio';
import { Stream } from 'types/service/audio';
const renderPlaybacks = (playbackDevices: Stream[]): PlaybackDevices => {
return playbackDevices.map((device) => {
if (device.description === 'Dummy Output') {
return Widget.Box({
class_name: 'menu-unfound-button playback',
child: Widget.Box({
children: [
Widget.Label({
class_name: 'menu-button-name playback',
label: 'No playback devices found...',
}),
],
}),
});
}
return Widget.Button({
class_name: `menu-button audio playback ${device}`,
on_primary_click: () => (audio.speaker = device),
child: Widget.Box({
children: [
Widget.Box({
hpack: 'start',
children: [
Widget.Label({
truncate: 'end',
wrap: true,
class_name: audio.speaker
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-icon active playback txt-icon'
: 'menu-button-icon playback txt-icon',
),
label: '',
}),
Widget.Label({
truncate: 'end',
wrap: true,
class_name: audio.speaker
.bind('description')
.as((v) =>
device.description === v
? 'menu-button-name active playback'
: 'menu-button-name playback',
),
label: device.description,
}),
],
}),
],
}),
});
});
};
export { renderPlaybacks };

View File

@@ -1,72 +0,0 @@
const audio = await Service.import('audio');
import { BoxWidget } from 'lib/types/widget.js';
import { renderInputDevices } from './InputDevices.js';
import { renderPlaybacks } from './PlaybackDevices.js';
const availableDevices = (): BoxWidget => {
return Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: 'menu-section-container playback',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-label-container playback',
hpack: 'fill',
child: Widget.Label({
class_name: 'menu-label audio playback',
hexpand: true,
hpack: 'start',
label: 'Playback Devices',
}),
}),
Widget.Box({
class_name: 'menu-items-section playback',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-container playback',
vertical: true,
children: [
Widget.Box({
vertical: true,
children: audio.bind('speakers').as((v) => renderPlaybacks(v)),
}),
],
}),
],
}),
Widget.Box({
class_name: 'menu-label-container input',
hpack: 'fill',
child: Widget.Label({
class_name: 'menu-label audio input',
hexpand: true,
hpack: 'start',
label: 'Input Devices',
}),
}),
Widget.Box({
class_name: 'menu-items-section input',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-container input',
vertical: true,
children: [
Widget.Box({
vertical: true,
children: audio.bind('microphones').as((v) => renderInputDevices(v)),
}),
],
}),
],
}),
],
}),
],
});
};
export { availableDevices };

View File

@@ -1,25 +0,0 @@
import Window from 'types/widgets/window.js';
import DropdownMenu from '../shared/dropdown/index.js';
import { activeDevices } from './active/index.js';
import { availableDevices } from './available/index.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options.js';
export default (): Window<Child, Attribute> => {
return DropdownMenu({
name: 'audiomenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'menu-items audio',
hpack: 'fill',
hexpand: true,
child: Widget.Box({
vertical: true,
hpack: 'fill',
hexpand: true,
class_name: 'menu-items-container audio',
children: [activeDevices(), availableDevices()],
}),
}),
});
};

View File

@@ -1,71 +0,0 @@
import { BoxWidget } from 'lib/types/widget';
import { BluetoothDevice } from 'types/service/bluetooth';
const connectedControls = (dev: BluetoothDevice, connectedDevices: BluetoothDevice[]): BoxWidget => {
if (!connectedDevices.includes(dev.address)) {
return Widget.Box({});
}
return Widget.Box({
vpack: 'start',
class_name: 'bluetooth-controls',
children: [
Widget.Button({
class_name: 'menu-icon-button unpair bluetooth',
child: Widget.Label({
tooltip_text: dev.paired ? 'Unpair' : 'Pair',
class_name: 'menu-icon-button-label unpair bluetooth txt-icon',
label: dev.paired ? '' : '',
}),
on_primary_click: () =>
Utils.execAsync([
'bash',
'-c',
`bluetoothctl ${dev.paired ? 'unpair' : 'pair'} ${dev.address}`,
]).catch((err) =>
console.error(`bluetoothctl ${dev.paired ? 'unpair' : 'pair'} ${dev.address}`, err),
),
}),
Widget.Button({
class_name: 'menu-icon-button disconnect bluetooth',
child: Widget.Label({
tooltip_text: dev.connected ? 'Disconnect' : 'Connect',
class_name: 'menu-icon-button-label disconnect bluetooth txt-icon',
label: dev.connected ? '󱘖' : '',
}),
on_primary_click: () => dev.setConnection(!dev.connected),
}),
Widget.Button({
class_name: 'menu-icon-button untrust bluetooth',
child: Widget.Label({
tooltip_text: dev.trusted ? 'Untrust' : 'Trust',
class_name: 'menu-icon-button-label untrust bluetooth txt-icon',
label: dev.trusted ? '' : '󱖡',
}),
on_primary_click: () =>
Utils.execAsync([
'bash',
'-c',
`bluetoothctl ${dev.trusted ? 'untrust' : 'trust'} ${dev.address}`,
]).catch((err) =>
console.error(`bluetoothctl ${dev.trusted ? 'untrust' : 'trust'} ${dev.address}`, err),
),
}),
Widget.Button({
class_name: 'menu-icon-button delete bluetooth',
child: Widget.Label({
tooltip_text: 'Forget',
class_name: 'menu-icon-button-label delete bluetooth txt-icon',
label: '󰆴',
}),
on_primary_click: () => {
Utils.execAsync(['bash', '-c', `bluetoothctl remove ${dev.address}`]).catch((err) =>
console.error('Bluetooth Remove', err),
);
},
}),
],
});
};
export { connectedControls };

View File

@@ -1,188 +0,0 @@
import { Bluetooth, BluetoothDevice } from 'types/service/bluetooth.js';
import Box from 'types/widgets/box.js';
import { connectedControls } from './connectedControls.js';
import { getBluetoothIcon } from '../utils.js';
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options';
import Label from 'types/widgets/label.js';
const { showBattery, batteryIcon, batteryState } = options.menus.bluetooth;
const devices = (bluetooth: Bluetooth, self: Box<Gtk.Widget, unknown>): Box<Child, Attribute> => {
return self.hook(bluetooth, () => {
if (!bluetooth.enabled) {
return (self.child = Widget.Box({
class_name: 'bluetooth-items',
vertical: true,
expand: true,
vpack: 'center',
hpack: 'center',
children: [
Widget.Label({
class_name: 'bluetooth-disabled dim',
hexpand: true,
label: 'Bluetooth is disabled',
}),
],
}));
}
const availableDevices = bluetooth.devices
.filter((btDev) => btDev.name !== null)
.sort((a, b) => {
if (a.connected || a.paired) {
return -1;
}
if (b.connected || b.paired) {
return 1;
}
return b.name - a.name;
});
const conDevNames = availableDevices.filter((d) => d.connected || d.paired).map((d) => d.address);
if (!availableDevices.length) {
return (self.child = Widget.Box({
class_name: 'bluetooth-items',
vertical: true,
expand: true,
vpack: 'center',
hpack: 'center',
children: [
Widget.Label({
class_name: 'no-bluetooth-devices dim',
hexpand: true,
label: 'No devices currently found',
}),
Widget.Label({
class_name: 'search-bluetooth-label dim',
hexpand: true,
label: "Press '󰑐' to search",
}),
],
}));
}
const getConnectionStatusLabel = (device: BluetoothDevice): Label<Attribute> => {
return Widget.Label({
hpack: 'start',
class_name: 'connection-status dim',
label: device.connected ? 'Connected' : 'Paired',
});
};
const getBatteryInfo = (device: BluetoothDevice, batIcon: string): Gtk.Widget[] => {
if (typeof device.battery_percentage === 'number' && device.battery_percentage >= 0) {
return [
Widget.Separator({
class_name: 'menu-separator bluetooth-battery',
}),
Widget.Label({
class_name: 'connection-status txt-icon',
label: `${batIcon}`,
}),
Widget.Label({
class_name: 'connection-status battery',
label: `${device.battery_percentage}%`,
}),
];
}
return [];
};
return (self.child = Widget.Box({
vertical: true,
children: availableDevices.map((device) => {
return Widget.Box({
children: [
Widget.Button({
hexpand: true,
class_name: `bluetooth-element-item ${device}`,
on_primary_click: () => {
if (!conDevNames.includes(device.address)) device.setConnection(true);
},
child: Widget.Box({
hexpand: true,
children: [
Widget.Box({
hexpand: true,
hpack: 'start',
class_name: 'menu-button-container',
children: [
Widget.Label({
vpack: 'start',
class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? 'active' : ''} txt-icon`,
label: getBluetoothIcon(`${device['icon_name']}-symbolic`),
}),
Widget.Box({
vertical: true,
vpack: 'center',
children: [
Widget.Label({
vpack: 'center',
hpack: 'start',
class_name: 'menu-button-name bluetooth',
truncate: 'end',
wrap: true,
label: device.alias,
}),
Widget.Revealer({
hpack: 'start',
reveal_child: device.connected || device.paired,
child: Widget.Box({
hpack: 'start',
children: Utils.merge(
[
showBattery.bind('value'),
batteryIcon.bind('value'),
batteryState.bind('value'),
],
(showBat, batIcon, batState) => {
if (
!showBat ||
(batState === 'paired' && !device.paired) ||
(batState === 'connected' && !device.connected)
) {
return [getConnectionStatusLabel(device)];
}
return [
getConnectionStatusLabel(device),
Widget.Box({
children: getBatteryInfo(device, batIcon),
}),
];
},
),
}),
}),
],
}),
],
}),
Widget.Box({
hpack: 'end',
children: device.connecting
? [
Widget.Spinner({
vpack: 'start',
class_name: 'spinner bluetooth',
}),
]
: [],
}),
],
}),
}),
connectedControls(device, conDevNames),
],
});
}),
}));
});
};
export { devices };

View File

@@ -1,26 +0,0 @@
const bluetooth = await Service.import('bluetooth');
import { label } from './label.js';
import { devices } from './devicelist.js';
import { BoxWidget } from 'lib/types/widget.js';
const Devices = (): BoxWidget => {
return Widget.Box({
class_name: 'menu-section-container',
vertical: true,
children: [
label(bluetooth),
Widget.Box({
class_name: 'menu-items-section',
child: Widget.Box({
class_name: 'menu-content',
vertical: true,
setup: (self) => {
devices(bluetooth, self);
},
}),
}),
],
});
};
export { Devices };

View File

@@ -1,65 +0,0 @@
import { BoxWidget } from 'lib/types/widget';
import { Bluetooth } from 'types/service/bluetooth';
const label = (bluetooth: Bluetooth): BoxWidget => {
const searchInProgress = Variable(false);
const startRotation = (): void => {
searchInProgress.value = true;
setTimeout(() => {
searchInProgress.value = false;
}, 10 * 1000);
};
return Widget.Box({
class_name: 'menu-label-container',
hpack: 'fill',
vpack: 'start',
children: [
Widget.Label({
class_name: 'menu-label',
vpack: 'center',
hpack: 'start',
label: 'Bluetooth',
}),
Widget.Box({
class_name: 'controls-container',
vpack: 'start',
children: [
Widget.Switch({
class_name: 'menu-switch bluetooth',
hexpand: true,
hpack: 'end',
active: bluetooth.bind('enabled'),
on_activate: ({ active }) => {
searchInProgress.value = false;
Utils.execAsync(['bash', '-c', `bluetoothctl power ${active ? 'on' : 'off'}`]).catch(
(err) => console.error(`bluetoothctl power ${active ? 'on' : 'off'}`, err),
);
},
}),
Widget.Separator({
class_name: 'menu-separator bluetooth',
}),
Widget.Button({
vpack: 'center',
class_name: 'menu-icon-button search',
on_primary_click: () => {
startRotation();
Utils.execAsync(['bash', '-c', 'bluetoothctl --timeout 120 scan on']).catch((err) => {
searchInProgress.value = false;
console.error('bluetoothctl --timeout 120 scan on', err);
});
},
child: Widget.Icon({
class_name: searchInProgress.bind('value').as((v) => (v ? 'spinning' : '')),
icon: 'view-refresh-symbolic',
}),
}),
],
}),
],
});
};
export { label };

View File

@@ -1,24 +0,0 @@
import Window from 'types/widgets/window.js';
import DropdownMenu from '../shared/dropdown/index.js';
import { Devices } from './devices/index.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options.js';
export default (): Window<Child, Attribute> => {
return DropdownMenu({
name: 'bluetoothmenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'menu-items bluetooth',
hpack: 'fill',
hexpand: true,
child: Widget.Box({
vertical: true,
hpack: 'fill',
hexpand: true,
class_name: 'menu-items-container bluetooth',
child: Devices(),
}),
}),
});
};

View File

@@ -1,24 +0,0 @@
import { BoxWidget } from 'lib/types/widget';
const CalendarWidget = (): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-item-container calendar',
hpack: 'fill',
vpack: 'fill',
expand: true,
child: Widget.Box({
class_name: 'calendar-container-box',
child: Widget.Calendar({
expand: true,
hpack: 'fill',
vpack: 'fill',
class_name: 'calendar-menu-widget',
showDayNames: true,
showDetails: false,
showHeading: true,
}),
}),
});
};
export { CalendarWidget };

View File

@@ -1,36 +0,0 @@
import DropdownMenu from 'modules/menus/shared/dropdown/index';
import { TimeWidget } from './time/index';
import { CalendarWidget } from './calendar';
import { WeatherWidget } from './weather/index';
import options from 'options';
import Window from 'types/widgets/window';
import { Attribute, Child } from 'lib/types/widget';
const { enabled: weatherEnabled } = options.menus.clock.weather;
export default (): Window<Child, Attribute> => {
return DropdownMenu({
name: 'calendarmenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'calendar-menu-content',
css: 'padding: 1px; margin: -1px;',
vexpand: false,
children: [
Widget.Box({
class_name: 'calendar-content-container',
vertical: true,
children: [
Widget.Box({
class_name: 'calendar-content-items',
vertical: true,
children: weatherEnabled.bind('value').as((isWeatherEnabled) => {
return [TimeWidget(), CalendarWidget(), ...(isWeatherEnabled ? [WeatherWidget()] : [])];
}),
}),
],
}),
],
}),
});
};

View File

@@ -1,75 +0,0 @@
import { BoxWidget } from 'lib/types/widget';
import options from 'options';
const { military, hideSeconds } = options.menus.clock.time;
const time = Variable('', {
poll: [1000, 'date "+%I:%M:%S"'],
});
const period = Variable('', {
poll: [1000, 'date "+%p"'],
});
const militaryTime = Variable('', {
poll: [1000, 'date "+%H:%M:%S"'],
});
const TimeWidget = (): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-item-container clock',
hexpand: true,
vpack: 'center',
hpack: 'fill',
child: Widget.Box({
hexpand: true,
vpack: 'center',
hpack: 'center',
class_name: 'clock-content-items',
children: Utils.merge(
[military.bind('value'), hideSeconds.bind('value')],
(is24hr: boolean, hideSeconds: boolean) => {
if (!is24hr) {
return [
Widget.Box({
hpack: 'center',
children: [
Widget.Label({
class_name: 'clock-content-time',
label: hideSeconds ? time.bind().as((str) => str.slice(0, -3)) : time.bind(),
}),
],
}),
Widget.Box({
hpack: 'center',
children: [
Widget.Label({
vpack: 'end',
class_name: 'clock-content-period',
label: period.bind(),
}),
],
}),
];
}
return [
Widget.Box({
hpack: 'center',
children: [
Widget.Label({
class_name: 'clock-content-time',
label: hideSeconds
? militaryTime.bind().as((str) => str.slice(0, -3))
: militaryTime.bind(),
}),
],
}),
];
},
),
}),
});
};
export { TimeWidget };

View File

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

View File

@@ -1,27 +0,0 @@
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import { HourlyIcon } from './icon/index.js';
import { HourlyTemp } from './temperature/index.js';
import { HourlyTime } from './time/index.js';
import { BoxWidget } from 'lib/types/widget.js';
export const Hourly = (theWeather: Variable<Weather>): BoxWidget => {
return Widget.Box({
vertical: false,
hexpand: true,
hpack: 'fill',
class_name: 'hourly-weather-container',
children: [1, 2, 3, 4].map((hoursFromNow) => {
return Widget.Box({
class_name: 'hourly-weather-item',
hexpand: true,
vertical: true,
children: [
HourlyTime(theWeather, hoursFromNow),
HourlyIcon(theWeather, hoursFromNow),
HourlyTemp(theWeather, hoursFromNow),
],
});
}),
});
};

View File

@@ -1,27 +0,0 @@
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import options from 'options';
import Label from 'types/widgets/label';
import { Child } from 'lib/types/widget';
import { getNextEpoch } from '../utils';
const { unit } = options.menus.clock.weather;
export const HourlyTemp = (theWeather: Variable<Weather>, hoursFromNow: number): Label<Child> => {
return Widget.Label({
class_name: 'hourly-weather-temp',
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
if (!Object.keys(wthr).length) {
return '-';
}
const nextEpoch = getNextEpoch(wthr, hoursFromNow);
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
if (unt === 'imperial') {
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : '-'}° F`;
}
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : '-'}° C`;
}),
});
};

View File

@@ -1,24 +0,0 @@
import { Weather } from 'lib/types/weather';
import { Child } from 'lib/types/widget';
import { Variable } from 'types/variable';
import Label from 'types/widgets/label';
import { getNextEpoch } from '../utils';
export const HourlyTime = (theWeather: Variable<Weather>, hoursFromNow: number): Label<Child> => {
return Widget.Label({
class_name: 'hourly-weather-time',
label: theWeather.bind('value').as((w) => {
if (!Object.keys(w).length) {
return '-';
}
const nextEpoch = getNextEpoch(w, hoursFromNow);
const dateAtEpoch = new Date(nextEpoch * 1000);
let hours = dateAtEpoch.getHours();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12 || 12;
return `${hours}${ampm}`;
}),
});
};

View File

@@ -1,20 +0,0 @@
import { Weather } from 'lib/types/weather';
export const getNextEpoch = (wthr: Weather, hoursFromNow: number): number => {
const currentEpoch = wthr.location.localtime_epoch;
const epochAtHourStart = currentEpoch - (currentEpoch % 3600);
let nextEpoch = 3600 * hoursFromNow + epochAtHourStart;
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;
}
return nextEpoch;
};

View File

@@ -1,18 +0,0 @@
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import { getWeatherStatusTextIcon } from 'globals/weather.js';
import { BoxWidget } from 'lib/types/widget';
export const TodayIcon = (theWeather: Variable<Weather>): BoxWidget => {
return Widget.Box({
vpack: 'center',
hpack: 'start',
class_name: 'calendar-menu-weather today icon container',
child: Widget.Label({
class_name: 'calendar-menu-weather today icon txt-icon',
label: theWeather.bind('value').as((w) => {
return getWeatherStatusTextIcon(w);
}),
}),
});
};

View File

@@ -1,38 +0,0 @@
import { TodayIcon } from './icon/index.js';
import { TodayStats } from './stats/index.js';
import { TodayTemperature } from './temperature/index.js';
import { Hourly } from './hourly/index.js';
import { globalWeatherVar } from 'globals/weather.js';
import { BoxWidget } from 'lib/types/widget.js';
const WeatherWidget = (): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-item-container weather',
child: Widget.Box({
class_name: 'weather-container-box',
setup: (self) => {
return (self.child = Widget.Box({
vertical: true,
hexpand: true,
children: [
Widget.Box({
class_name: 'calendar-menu-weather today',
hexpand: true,
children: [
TodayIcon(globalWeatherVar),
TodayTemperature(globalWeatherVar),
TodayStats(globalWeatherVar),
],
}),
Widget.Separator({
class_name: 'menu-separator weather',
}),
Hourly(globalWeatherVar),
],
}));
},
}),
});
};
export { WeatherWidget };

View File

@@ -1,50 +0,0 @@
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import options from 'options';
import { Unit } from 'lib/types/options';
import { getRainChance, getWindConditions } from 'globals/weather';
import { BoxWidget } from 'lib/types/widget';
const { unit } = options.menus.clock.weather;
export const TodayStats = (theWeather: Variable<Weather>): BoxWidget => {
return Widget.Box({
class_name: 'calendar-menu-weather today stats container',
hpack: 'end',
vpack: 'center',
vertical: true,
children: [
Widget.Box({
class_name: 'weather wind',
children: [
Widget.Label({
class_name: 'weather wind icon txt-icon',
label: '',
}),
Widget.Label({
class_name: 'weather wind label',
label: Utils.merge(
[theWeather.bind('value'), unit.bind('value')],
(wthr: Weather, unt: Unit) => {
return getWindConditions(wthr, unt);
},
),
}),
],
}),
Widget.Box({
class_name: 'weather precip',
children: [
Widget.Label({
class_name: 'weather precip icon txt-icon',
label: '',
}),
Widget.Label({
class_name: 'weather precip label',
label: theWeather.bind('value').as((v) => getRainChance(v)),
}),
],
}),
],
});
};

View File

@@ -1,62 +0,0 @@
import { Weather } from 'lib/types/weather';
import { Variable } from 'types/variable';
import options from 'options';
import { getTemperature, getWeatherIcon } from 'globals/weather';
import { BoxWidget } from 'lib/types/widget';
const { unit } = options.menus.clock.weather;
export const TodayTemperature = (theWeather: Variable<Weather>): BoxWidget => {
return Widget.Box({
hpack: 'center',
vpack: 'center',
vertical: true,
children: [
Widget.Box({
hexpand: true,
vpack: 'center',
class_name: 'calendar-menu-weather today temp container',
vertical: false,
children: [
Widget.Box({
hexpand: true,
hpack: 'center',
children: [
Widget.Label({
class_name: 'calendar-menu-weather today temp label',
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
return getTemperature(wthr, unt);
}),
}),
Widget.Label({
class_name: theWeather
.bind('value')
.as(
(v) =>
`calendar-menu-weather today temp label icon txt-icon ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
),
label: theWeather
.bind('value')
.as((v) => getWeatherIcon(Math.ceil(v.current.temp_f)).icon),
}),
],
}),
],
}),
Widget.Box({
hpack: 'center',
child: Widget.Label({
maxWidthChars: 15,
truncate: 'end',
lines: 2,
class_name: theWeather
.bind('value')
.as(
(v) =>
`calendar-menu-weather today condition label ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
),
label: theWeather.bind('value').as((v) => v.current.condition.text),
}),
}),
],
});
};

View File

@@ -1,113 +0,0 @@
import { BoxWidget } from 'lib/types/widget';
const network = await Service.import('network');
const bluetooth = await Service.import('bluetooth');
const notifications = await Service.import('notifications');
const audio = await Service.import('audio');
const Controls = (): BoxWidget => {
return Widget.Box({
class_name: 'dashboard-card controls-container',
hpack: 'fill',
vpack: 'fill',
expand: true,
children: [
Widget.Button({
tooltip_text: 'Toggle Wifi',
expand: true,
setup: (self) => {
self.hook(network, () => {
return (self.class_name = `dashboard-button wifi ${!network.wifi.enabled ? 'disabled' : ''}`);
});
},
on_primary_click: () => network.toggleWifi(),
child: Widget.Label({
class_name: 'txt-icon',
setup: (self) => {
self.hook(network, () => {
return (self.label = network.wifi.enabled ? '󰤨' : '󰤭');
});
},
}),
}),
Widget.Button({
tooltip_text: 'Toggle Bluetooth',
expand: true,
class_name: bluetooth
.bind('enabled')
.as((btOn) => `dashboard-button bluetooth ${!btOn ? 'disabled' : ''}`),
on_primary_click: () => bluetooth.toggle(),
child: Widget.Label({
class_name: 'txt-icon',
label: bluetooth.bind('enabled').as((btOn) => (btOn ? '󰂯' : '󰂲')),
}),
}),
Widget.Button({
tooltip_text: 'Toggle Notifications',
expand: true,
class_name: notifications
.bind('dnd')
.as((dnd) => `dashboard-button notifications ${dnd ? 'disabled' : ''}`),
on_primary_click: () => (notifications.dnd = !notifications.dnd),
child: Widget.Label({
class_name: 'txt-icon',
label: notifications.bind('dnd').as((dnd) => (dnd ? '󰂛' : '󰂚')),
}),
}),
Widget.Button({
tooltip_text: 'Toggle Mute (Playback)',
expand: true,
on_primary_click: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
setup: (self) => {
self.hook(
audio.speaker,
() => {
return (self.class_name = `dashboard-button playback ${audio.speaker.is_muted ? 'disabled' : ''}`);
},
'notify::is-muted',
);
},
child: Widget.Label({
class_name: 'txt-icon',
setup: (self) => {
self.hook(
audio.speaker,
() => {
return (self.label = audio.speaker.is_muted ? '󰖁' : '󰕾');
},
'notify::is-muted',
);
},
}),
}),
Widget.Button({
tooltip_text: 'Toggle Mute (Microphone)',
expand: true,
on_primary_click: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
setup: (self) => {
self.hook(
audio.microphone,
() => {
return (self.class_name = `dashboard-button input ${audio.microphone.is_muted ? 'disabled' : ''}`);
},
'notify::is-muted',
);
},
child: Widget.Label({
class_name: 'txt-icon',
setup: (self) => {
self.hook(
audio.microphone,
() => {
return (self.label = audio.microphone.is_muted ? '󰍭' : '󰍬');
},
'notify::is-muted',
);
},
}),
}),
],
});
};
export { Controls };

View File

@@ -1,121 +0,0 @@
import { BoxWidget } from 'lib/types/widget';
import options from 'options';
const { left, right } = options.menus.dashboard.directories;
const Directories = (): BoxWidget => {
return Widget.Box({
class_name: 'dashboard-card directories-container',
vpack: 'fill',
hpack: 'fill',
expand: true,
children: [
Widget.Box({
vertical: true,
expand: true,
class_name: 'section right',
children: [
Widget.Button({
hpack: 'start',
expand: true,
class_name: 'directory-link left top',
on_primary_click: left.directory1.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: left.directory1.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link left middle',
on_primary_click: left.directory2.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: left.directory2.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link left bottom',
on_primary_click: left.directory3.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: left.directory3.label.bind('value'),
}),
}),
],
}),
Widget.Box({
vertical: true,
expand: true,
class_name: 'section left',
children: [
Widget.Button({
hpack: 'start',
expand: true,
class_name: 'directory-link right top',
on_primary_click: right.directory1.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: right.directory1.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link right middle',
on_primary_click: right.directory2.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: right.directory2.label.bind('value'),
}),
}),
Widget.Button({
expand: true,
hpack: 'start',
class_name: 'directory-link right bottom',
on_primary_click: right.directory3.command.bind('value').as((cmd) => {
return () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(cmd);
};
}),
child: Widget.Label({
hpack: 'start',
label: right.directory3.label.bind('value'),
}),
}),
],
}),
],
});
};
export { Directories };

View File

@@ -1,52 +0,0 @@
import DropdownMenu from '../shared/dropdown/index.js';
import { Profile } from './profile/index.js';
import { Shortcuts } from './shortcuts/index.js';
import { Controls } from './controls/index.js';
import { Stats } from './stats/index.js';
import { Directories } from './directories/index.js';
import Window from 'types/widgets/window.js';
import { Attribute, Child } from 'lib/types/widget.js';
import options from 'options.js';
const { controls, shortcuts, stats, directories } = options.menus.dashboard;
export default (): Window<Child, Attribute> => {
return DropdownMenu({
name: 'dashboardmenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'dashboard-menu-content',
css: 'padding: 1px; margin: -1px;',
vexpand: false,
children: [
Widget.Box({
class_name: 'dashboard-content-container',
vertical: true,
children: Utils.merge(
[
controls.enabled.bind('value'),
shortcuts.enabled.bind('value'),
stats.enabled.bind('value'),
directories.enabled.bind('value'),
],
(isControlsEnabled, isShortcutsEnabled, isStatsEnabled, isDirectoriesEnabled) => {
return [
Widget.Box({
class_name: 'dashboard-content-items',
vertical: true,
children: [
Profile(),
...(isShortcutsEnabled ? [Shortcuts()] : []),
...(isControlsEnabled ? [Controls()] : []),
...(isDirectoriesEnabled ? [Directories()] : []),
...(isStatsEnabled ? [Stats()] : []),
],
}),
];
},
),
}),
],
}),
});
};

View File

@@ -1,110 +0,0 @@
import powermenu from '../../power/helpers/actions.js';
import { PowerOptions } from 'lib/types/options.js';
import GdkPixbuf from 'gi://GdkPixbuf';
import options from 'options';
import { BoxWidget, Child } from 'lib/types/widget.js';
import Label from 'types/widgets/label.js';
const { image, name } = options.menus.dashboard.powermenu.avatar;
const { confirmation, shutdown, logout, sleep, reboot } = options.menus.dashboard.powermenu;
const Profile = (): BoxWidget => {
const handleClick = (action: PowerOptions): void => {
const actions = {
shutdown: shutdown.value,
reboot: reboot.value,
logout: logout.value,
sleep: sleep.value,
};
App.closeWindow('dashboardmenu');
if (!confirmation.value) {
Utils.execAsync(actions[action]).catch((err) =>
console.error(`Failed to execute ${action} command. Error: ${err}`),
);
} else {
powermenu.action(action);
}
};
const getIconForButton = (txtIcon: string): Label<Child> => {
return Widget.Label({
className: 'txt-icon',
label: txtIcon,
});
};
return Widget.Box({
class_name: 'profiles-container',
hpack: 'fill',
hexpand: true,
children: [
Widget.Box({
class_name: 'profile-picture-container dashboard-card',
hexpand: true,
vertical: true,
children: [
Widget.Box({
hpack: 'center',
class_name: 'profile-picture',
css: image.bind('value').as((i) => {
try {
GdkPixbuf.Pixbuf.new_from_file(i);
return `background-image: url("${i}")`;
} catch {
return `background-image: url("${App.configDir}/assets/hyprpanel.png")`;
}
}),
}),
Widget.Label({
hpack: 'center',
class_name: 'profile-name',
label: name.bind('value').as((v) => {
if (v === 'system') {
return Utils.exec('bash -c whoami');
}
return v;
}),
}),
],
}),
Widget.Box({
class_name: 'power-menu-container dashboard-card',
vertical: true,
vexpand: true,
children: [
Widget.Button({
class_name: 'dashboard-button shutdown',
on_clicked: () => handleClick('shutdown'),
tooltip_text: 'Shut Down',
vexpand: true,
child: getIconForButton('󰐥'),
}),
Widget.Button({
class_name: 'dashboard-button restart',
on_clicked: () => handleClick('reboot'),
tooltip_text: 'Restart',
vexpand: true,
child: getIconForButton('󰜉'),
}),
Widget.Button({
class_name: 'dashboard-button lock',
on_clicked: () => handleClick('logout'),
tooltip_text: 'Log Out',
vexpand: true,
child: getIconForButton('󰿅'),
}),
Widget.Button({
class_name: 'dashboard-button sleep',
on_clicked: () => handleClick('sleep'),
tooltip_text: 'Sleep',
vexpand: true,
child: getIconForButton('󰤄'),
}),
],
}),
],
});
};
export { Profile };

View File

@@ -1,326 +0,0 @@
const hyprland = await Service.import('hyprland');
import { BashPoller } from 'lib/poller/BashPoller';
import { Attribute, BoxWidget, Child } from 'lib/types/widget';
import options from 'options';
import { Variable as VarType } from 'types/variable';
import Box from 'types/widgets/box';
import Button from 'types/widgets/button';
import Label from 'types/widgets/label';
const { left, right } = options.menus.dashboard.shortcuts;
const Shortcuts = (): BoxWidget => {
const pollingInterval = Variable(1000);
const isRecording = Variable(false);
const handleRecorder = (commandOutput: string): boolean => {
if (commandOutput === 'recording') {
return true;
}
return false;
};
const recordingPoller = new BashPoller<boolean, []>(
isRecording,
[],
pollingInterval.bind('value'),
`${App.configDir}/services/screen_record.sh status`,
handleRecorder,
);
recordingPoller.initialize();
const handleClick = (action: string, tOut: number = 250): void => {
App.closeWindow('dashboardmenu');
setTimeout(() => {
Utils.execAsync(action)
.then((res) => {
return res;
})
.catch((err) => err);
}, tOut);
};
const recordingDropdown = Widget.Menu({
class_name: 'dropdown recording',
hpack: 'fill',
hexpand: true,
setup: (self) => {
const renderMonitorList = (): void => {
const displays = hyprland.monitors.map((mon) => {
return Widget.MenuItem({
label: `Display ${mon.name}`,
on_activate: () => {
App.closeWindow('dashboardmenu');
Utils.execAsync(`${App.configDir}/services/screen_record.sh start ${mon.name}`).catch(
(err) => console.error(err),
);
},
});
});
// NOTE: This is disabled since window recording isn't available on wayland
// const apps = hyprland.clients.map((clt) => {
// return Widget.MenuItem({
// label: `${clt.class.charAt(0).toUpperCase() + clt.class.slice(1)} (Workspace ${clt.workspace.name})`,
// on_activate: () => {
// App.closeWindow('dashboardmenu');
// Utils.execAsync(
// `${App.configDir}/services/screen_record.sh start ${clt.focusHistoryID}`,
// ).catch((err) => console.error(err));
// },
// });
// });
self.children = [
...displays,
// Disabled since window recording isn't available on wayland
// ...apps
];
};
self.hook(hyprland, renderMonitorList, 'monitor-added');
self.hook(hyprland, renderMonitorList, 'monitor-removed');
},
});
type ShortcutFixed = {
tooltip: string;
command: string;
icon: string;
configurable: false;
};
type ShortcutVariable = {
tooltip: VarType<string>;
command: VarType<string>;
icon: VarType<string>;
configurable?: true;
};
type Shortcut = ShortcutFixed | ShortcutVariable;
const cmdLn = (sCut: ShortcutVariable): boolean => {
return sCut.command.value.length > 0;
};
const leftCardHidden = Variable(
!(cmdLn(left.shortcut1) || cmdLn(left.shortcut2) || cmdLn(left.shortcut3) || cmdLn(left.shortcut4)),
);
const createButton = (shortcut: Shortcut, className: string): Button<Label<Attribute>, Attribute> => {
if (shortcut.configurable !== false) {
return Widget.Button({
vexpand: true,
tooltip_text: shortcut.tooltip.value,
class_name: className,
on_primary_click: () => handleClick(shortcut.command.value),
child: Widget.Label({
class_name: 'button-label txt-icon',
label: shortcut.icon.value,
}),
});
} else {
// handle non-configurable shortcut
return Widget.Button({
vexpand: true,
tooltip_text: shortcut.tooltip,
class_name: className,
on_primary_click: (_, event) => {
if (shortcut.command === 'settings-dialog') {
App.closeWindow('dashboardmenu');
App.toggleWindow('settings-dialog');
} else if (shortcut.command === 'record') {
if (isRecording.value === true) {
App.closeWindow('dashboardmenu');
return Utils.execAsync(`${App.configDir}/services/screen_record.sh stop`).catch((err) =>
console.error(err),
);
} else {
recordingDropdown.popup_at_pointer(event);
}
}
},
child: Widget.Label({
class_name: 'button-label txt-icon',
label: shortcut.icon,
}),
});
}
};
const createButtonIfCommandExists = (
shortcut: Shortcut,
className: string,
command: string,
): Button<Label<Attribute>, Attribute> | Box<Child, Attribute> => {
if (command.length > 0) {
return createButton(shortcut, className);
}
return Widget.Box();
};
return Widget.Box({
class_name: 'shortcuts-container',
hpack: 'fill',
hexpand: true,
children: [
Widget.Box({
child: Utils.merge(
[
left.shortcut1.command.bind('value'),
left.shortcut2.command.bind('value'),
left.shortcut1.tooltip.bind('value'),
left.shortcut2.tooltip.bind('value'),
left.shortcut1.icon.bind('value'),
left.shortcut2.icon.bind('value'),
left.shortcut3.command.bind('value'),
left.shortcut4.command.bind('value'),
left.shortcut3.tooltip.bind('value'),
left.shortcut4.tooltip.bind('value'),
left.shortcut3.icon.bind('value'),
left.shortcut4.icon.bind('value'),
],
() => {
const isVisibleLeft = cmdLn(left.shortcut1) || cmdLn(left.shortcut2);
const isVisibleRight = cmdLn(left.shortcut3) || cmdLn(left.shortcut4);
if (!isVisibleLeft && !isVisibleRight) {
leftCardHidden.value = true;
return Widget.Box();
}
leftCardHidden.value = false;
return Widget.Box({
class_name: 'container most-used dashboard-card',
children: [
Widget.Box({
className: `card-button-section-container ${isVisibleRight && isVisibleLeft ? 'visible' : ''}`,
child: isVisibleLeft
? Widget.Box({
vertical: true,
hexpand: true,
vexpand: true,
children: [
createButtonIfCommandExists(
left.shortcut1,
`dashboard-button top-button ${cmdLn(left.shortcut2) ? 'paired' : ''}`,
left.shortcut1.command.value,
),
createButtonIfCommandExists(
left.shortcut2,
'dashboard-button',
left.shortcut2.command.value,
),
],
})
: Widget.Box({
children: [],
}),
}),
Widget.Box({
className: 'card-button-section-container',
child: isVisibleRight
? Widget.Box({
vertical: true,
hexpand: true,
vexpand: true,
children: [
createButtonIfCommandExists(
left.shortcut3,
`dashboard-button top-button ${cmdLn(left.shortcut4) ? 'paired' : ''}`,
left.shortcut3.command.value,
),
createButtonIfCommandExists(
left.shortcut4,
'dashboard-button',
left.shortcut4.command.value,
),
],
})
: Widget.Box({
children: [],
}),
}),
],
});
},
),
}),
Widget.Box({
child: Utils.merge(
[
right.shortcut1.command.bind('value'),
right.shortcut1.tooltip.bind('value'),
right.shortcut1.icon.bind('value'),
right.shortcut3.command.bind('value'),
right.shortcut3.tooltip.bind('value'),
right.shortcut3.icon.bind('value'),
leftCardHidden.bind('value'),
isRecording.bind('value'),
],
() => {
return Widget.Box({
class_name: `container utilities dashboard-card ${!leftCardHidden.value ? 'paired' : ''}`,
children: [
Widget.Box({
className: `card-button-section-container visible`,
child: Widget.Box({
vertical: true,
hexpand: true,
vexpand: true,
children: [
createButtonIfCommandExists(
right.shortcut1,
'dashboard-button top-button paired',
right.shortcut1.command.value,
),
createButtonIfCommandExists(
{
tooltip: 'HyprPanel Configuration',
command: 'settings-dialog',
icon: '󰒓',
configurable: false,
},
'dashboard-button',
'settings-dialog',
),
],
}),
}),
Widget.Box({
className: 'card-button-section-container',
child: Widget.Box({
vertical: true,
hexpand: true,
vexpand: true,
children: [
createButtonIfCommandExists(
right.shortcut3,
'dashboard-button top-button paired',
right.shortcut3.command.value,
),
createButtonIfCommandExists(
{
tooltip: 'Record Screen',
command: 'record',
icon: '󰑊',
configurable: false,
},
`dashboard-button record ${isRecording.value ? 'active' : ''}`,
'record',
),
],
}),
}),
],
});
},
),
}),
],
});
};
export { Shortcuts };

View File

@@ -1,265 +0,0 @@
import options from 'options';
import Ram from 'services/Ram';
import { GPU_Stat } from 'lib/types/gpustat';
import { dependencies } from 'lib/utils';
import { BoxWidget } from 'lib/types/widget';
import Cpu from 'services/Cpu';
import Storage from 'services/Storage';
import { renderResourceLabel } from 'customModules/utils';
const { terminal } = options;
const { enable_gpu, interval } = options.menus.dashboard.stats;
const ramService = new Ram();
const cpuService = new Cpu();
const storageService = new Storage();
ramService.setShouldRound(true);
storageService.setShouldRound(true);
interval.connect('changed', () => {
ramService.updateTimer(interval.value);
cpuService.updateTimer(interval.value);
storageService.updateTimer(interval.value);
});
const handleClick = (): void => {
App.closeWindow('dashboardmenu');
Utils.execAsync(`bash -c "${terminal} -e btop"`).catch((err) => `Failed to open btop: ${err}`);
};
const Stats = (): BoxWidget => {
const divide = ([total, free]: number[]): number => free / total;
const gpu = Variable(0);
const GPUStat = Widget.Box({
child: enable_gpu.bind('value').as((gpStat) => {
if (!gpStat || !dependencies('gpustat')) {
return Widget.Box();
}
return Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: 'stat gpu',
hexpand: true,
vpack: 'center',
setup: (self) => {
const getGpuUsage = (): void => {
if (!enable_gpu.value) {
gpu.value = 0;
return;
}
Utils.execAsync('gpustat --json')
.then((out) => {
if (typeof out !== 'string') {
return 0;
}
try {
const data = JSON.parse(out);
const totalGpu = 100;
const usedGpu =
data.gpus.reduce((acc: number, gpu: GPU_Stat) => {
return acc + gpu['utilization.gpu'];
}, 0) / data.gpus.length;
gpu.value = divide([totalGpu, usedGpu]);
} catch (e) {
console.error('Error getting GPU stats:', e);
gpu.value = 0;
}
})
.catch((err) => {
console.error(`An error occurred while fetching GPU stats: ${err}`);
});
};
self.poll(2000, getGpuUsage);
Utils.merge([gpu.bind('value'), enable_gpu.bind('value')], (gpu, enableGpu) => {
if (!enableGpu) {
return (self.children = []);
}
return (self.children = [
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.Label({
class_name: 'txt-icon',
label: '󰢮',
}),
}),
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.LevelBar({
class_name: 'stats-bar',
hexpand: true,
vpack: 'center',
value: gpu,
}),
}),
]);
});
},
}),
Widget.Box({
hpack: 'end',
children: Utils.merge([gpu.bind('value'), enable_gpu.bind('value')], (gpuUsed, enableGpu) => {
if (!enableGpu) {
return [];
}
return [
Widget.Label({
class_name: 'stat-value gpu',
label: `${Math.floor(gpuUsed * 100)}%`,
}),
];
}),
}),
],
});
}),
});
return Widget.Box({
class_name: 'dashboard-card stats-container',
vertical: true,
vpack: 'fill',
hpack: 'fill',
expand: true,
children: [
Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: 'stat cpu',
hexpand: true,
vpack: 'center',
children: [
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.Label({
class_name: 'txt-icon',
label: '',
}),
}),
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.LevelBar({
class_name: 'stats-bar',
hexpand: true,
vpack: 'center',
bar_mode: 'continuous',
max_value: 1,
value: cpuService.cpu.bind('value').as((cpuUsage) => Math.round(cpuUsage) / 100),
}),
}),
],
}),
Widget.Label({
hpack: 'end',
class_name: 'stat-value cpu',
label: cpuService.cpu.bind('value').as((cpuUsage) => `${Math.round(cpuUsage)}%`),
}),
],
}),
Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: 'stat ram',
vpack: 'center',
hexpand: true,
children: [
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.Label({
class_name: 'txt-icon',
label: '',
}),
}),
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.LevelBar({
class_name: 'stats-bar',
hexpand: true,
vpack: 'center',
value: ramService.ram.bind('value').as((ramUsage) => {
return ramUsage.percentage / 100;
}),
}),
}),
],
}),
Widget.Label({
hpack: 'end',
class_name: 'stat-value ram',
label: ramService.ram
.bind('value')
.as((ramUsage) => `${renderResourceLabel('used/total', ramUsage, true)}`),
}),
],
}),
GPUStat,
Widget.Box({
vertical: true,
children: [
Widget.Box({
class_name: 'stat storage',
hexpand: true,
vpack: 'center',
children: [
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.Label({
class_name: 'txt-icon',
label: '󰋊',
}),
}),
Widget.Button({
on_primary_click: () => {
handleClick();
},
child: Widget.LevelBar({
class_name: 'stats-bar',
hexpand: true,
vpack: 'center',
value: storageService.storage
.bind('value')
.as((storageUsage) => storageUsage.percentage / 100),
}),
}),
],
}),
Widget.Label({
hpack: 'end',
class_name: 'stat-value storage',
label: storageService.storage
.bind('value')
.as((storageUsage) => `${renderResourceLabel('used/total', storageUsage, true)}`),
}),
],
}),
],
});
};
export { Stats };

View File

@@ -1,58 +0,0 @@
import { BoxWidget } from 'lib/types/widget.js';
import brightness from '../../../../services/Brightness.js';
import icons from '../../../icons/index.js';
const Brightness = (): BoxWidget => {
return Widget.Box({
class_name: 'menu-section-container brightness',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-label-container',
hpack: 'fill',
child: Widget.Label({
class_name: 'menu-label',
hexpand: true,
hpack: 'start',
label: 'Brightness',
}),
}),
Widget.Box({
class_name: 'menu-items-section',
vpack: 'fill',
vexpand: true,
vertical: true,
child: Widget.Box({
class_name: 'brightness-container',
children: [
Widget.Icon({
vexpand: true,
vpack: 'center',
class_name: 'brightness-slider-icon',
icon: icons.brightness.screen,
}),
Widget.Slider({
vpack: 'center',
vexpand: true,
value: brightness.bind('screen'),
class_name: 'menu-active-slider menu-slider brightness',
draw_value: false,
hexpand: true,
min: 0,
max: 1,
onChange: ({ value }) => (brightness.screen = value),
}),
Widget.Label({
vpack: 'center',
vexpand: true,
class_name: 'brightness-slider-label',
label: brightness.bind('screen').as((b) => `${Math.round(b * 100)}%`),
}),
],
}),
}),
],
});
};
export { Brightness };

View File

@@ -1,25 +0,0 @@
import DropdownMenu from '../shared/dropdown/index.js';
import { EnergyProfiles } from './profiles/index.js';
import { Brightness } from './brightness/index.js';
import { Attribute, Child } from 'lib/types/widget.js';
import Window from 'types/widgets/window.js';
import options from 'options.js';
export default (): Window<Child, Attribute> => {
return DropdownMenu({
name: 'energymenu',
transition: options.menus.transition.bind('value'),
child: Widget.Box({
class_name: 'menu-items energy',
hpack: 'fill',
hexpand: true,
child: Widget.Box({
vertical: true,
hpack: 'fill',
hexpand: true,
class_name: 'menu-items-container energy',
children: [Brightness(), EnergyProfiles()],
}),
}),
});
};

View File

@@ -1,85 +0,0 @@
const powerProfiles = await Service.import('powerprofiles');
import { PowerProfile, PowerProfileObject, PowerProfiles } from 'lib/types/powerprofiles.js';
import { BoxWidget } from 'lib/types/widget.js';
import icons from '../../../icons/index.js';
import { uptime } from 'lib/variables.js';
const EnergyProfiles = (): BoxWidget => {
const isValidProfile = (profile: string): profile is PowerProfile =>
profile === 'power-saver' || profile === 'balanced' || profile === 'performance';
function renderUptime(curUptime: number): string {
const days = Math.floor(curUptime / (60 * 24));
const hours = Math.floor((curUptime % (60 * 24)) / 60);
const minutes = Math.floor(curUptime % 60);
return ` : ${days}d ${hours}h ${minutes}m`;
}
return Widget.Box({
class_name: 'menu-section-container energy',
vertical: true,
children: [
Widget.Box({
class_name: 'menu-label-container',
hpack: 'fill',
children: [
Widget.Label({
class_name: 'menu-label',
hexpand: true,
hpack: 'start',
label: 'Power Profile',
}),
Widget.Label({
class_name: 'menu-label uptime',
label: uptime.bind().as(renderUptime),
tooltipText: 'Uptime',
}),
],
}),
Widget.Box({
class_name: 'menu-items-section',
vpack: 'fill',
vexpand: true,
vertical: true,
children: powerProfiles.bind('profiles').as((profiles: PowerProfiles) => {
return profiles.map((prof: PowerProfileObject) => {
const profileLabels = {
'power-saver': 'Power Saver',
balanced: 'Balanced',
performance: 'Performance',
};
const profileType = prof.Profile;
if (!isValidProfile(profileType)) {
return profileLabels.balanced;
}
return Widget.Button({
on_primary_click: () => {
powerProfiles.active_profile = prof.Profile;
},
class_name: powerProfiles.bind('active_profile').as((active) => {
return `power-profile-item ${active === prof.Profile ? 'active' : ''}`;
}),
child: Widget.Box({
children: [
Widget.Icon({
class_name: 'power-profile-icon',
icon: icons.powerprofile[profileType],
}),
Widget.Label({
class_name: 'power-profile-label',
label: profileLabels[profileType],
}),
],
}),
});
});
}),
}),
],
});
};
export { EnergyProfiles };

View File

@@ -1,22 +0,0 @@
import { BoxWidget } from 'lib/types/widget.js';
import { shuffleControl } from './shuffle/index.js';
import { previousTrack } from './previous/index.js';
import { playPause } from './playpause/index.js';
import { nextTrack } from './next/index.js';
import { loopControl } from './loop/index.js';
const Controls = (): BoxWidget => {
return Widget.Box({
class_name: 'media-indicator-current-player-controls',
vertical: true,
children: [
Widget.Box({
class_name: 'media-indicator-current-controls',
hpack: 'center',
children: [shuffleControl(), previousTrack(), playPause(), nextTrack(), loopControl()],
}),
],
});
};
export { Controls };

Some files were not shown because too many files have changed in this diff Show More