diff --git a/.eslintrc.js b/.eslintrc.js index b650cfe7c..936a43579 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27ec51a37..66d3fdcab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore index 9144fbaa8..0ecea5704 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .weather.json node_modules + +@girs diff --git a/.gitmodules b/.gitmodules index b165aa597..6050ac8f9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 + diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 396664cdc..000000000 --- a/PKGBUILD +++ /dev/null @@ -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 - -# 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 -} diff --git a/app.ts b/app.ts new file mode 100644 index 000000000..c7516ae80 --- /dev/null +++ b/app.ts @@ -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); + } +}); diff --git a/astal b/astal new file mode 120000 index 000000000..ded1b3287 --- /dev/null +++ b/astal @@ -0,0 +1 @@ +/usr/share/astal/gjs \ No newline at end of file diff --git a/config.js b/config.js deleted file mode 100644 index 01d6ae869..000000000 --- a/config.js +++ /dev/null @@ -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 {}; diff --git a/customModules/config.ts b/customModules/config.ts deleted file mode 100644 index 4452a4358..000000000 --- a/customModules/config.ts +++ /dev/null @@ -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 => - 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', - }), - ], - }), - }); diff --git a/customModules/cpu/index.ts b/customModules/cpu/index.ts deleted file mode 100644 index c5e5c19db..000000000 --- a/customModules/cpu/index.ts +++ /dev/null @@ -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( - // 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) => { - inputHandler(self, { - onPrimaryClick: { - cmd: leftClick, - }, - onSecondaryClick: { - cmd: rightClick, - }, - onMiddleClick: { - cmd: middleClick, - }, - onScrollUp: { - cmd: scrollUp, - }, - onScrollDown: { - cmd: scrollDown, - }, - }); - }, - }, - }); - - return cpuModule; -}; diff --git a/customModules/cputemp/helpers.ts b/customModules/cputemp/helpers.ts deleted file mode 100644 index 1d451cecb..000000000 --- a/customModules/cputemp/helpers.ts +++ /dev/null @@ -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, unit: VariableType): 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; - } -}; diff --git a/customModules/cputemp/index.ts b/customModules/cputemp/index.ts deleted file mode 100644 index d42753d9c..000000000 --- a/customModules/cputemp/index.ts +++ /dev/null @@ -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, VariableType]>( - // 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) => { - inputHandler(self, { - onPrimaryClick: { - cmd: leftClick, - }, - onSecondaryClick: { - cmd: rightClick, - }, - onMiddleClick: { - cmd: middleClick, - }, - onScrollUp: { - cmd: scrollUp, - }, - onScrollDown: { - cmd: scrollDown, - }, - }); - }, - }, - }); - - return cpuTempModule; -}; diff --git a/customModules/hypridle/helpers.ts b/customModules/hypridle/helpers.ts deleted file mode 100644 index d147dd4ec..000000000 --- a/customModules/hypridle/helpers.ts +++ /dev/null @@ -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): 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'; - }); -}; diff --git a/customModules/hyprsunset/helpers.ts b/customModules/hyprsunset/helpers.ts deleted file mode 100644 index 2a62586b5..000000000 --- a/customModules/hyprsunset/helpers.ts +++ /dev/null @@ -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): 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'; - }); -}; diff --git a/customModules/module.ts b/customModules/module.ts deleted file mode 100644 index 39ade5fba..000000000 --- a/customModules/module.ts +++ /dev/null @@ -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, - }; -}; diff --git a/customModules/netstat/index.ts b/customModules/netstat/index.ts deleted file mode 100644 index 120662ef8..000000000 --- a/customModules/netstat/index.ts +++ /dev/null @@ -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(GET_DEFAULT_NETSTAT_DATA(rateUnit.value)); - -const netstatPoller = new FunctionPoller< - NetworkResourceData, - [round: TVariable, interfaceNameVar: TVariable, dataType: TVariable] ->( - // 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) => { - 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; -}; diff --git a/customModules/ram/index.ts b/customModules/ram/index.ts deleted file mode 100644 index 2b380f61e..000000000 --- a/customModules/ram/index.ts +++ /dev/null @@ -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(defaultRamData); - -const ramPoller = new FunctionPoller]>( - 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) => { - 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; -}; diff --git a/customModules/storage/computeStorage.ts b/customModules/storage/computeStorage.ts deleted file mode 100644 index f30ede5ab..000000000 --- a/customModules/storage/computeStorage.ts +++ /dev/null @@ -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): 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 }; - } -}; diff --git a/customModules/storage/index.ts b/customModules/storage/index.ts deleted file mode 100644 index af91233fe..000000000 --- a/customModules/storage/index.ts +++ /dev/null @@ -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(defaultStorageData); - -const storagePoller = new FunctionPoller]>( - 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) => { - 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; -}; diff --git a/customModules/submap/helpers.ts b/customModules/submap/helpers.ts deleted file mode 100644 index 0c38718c2..000000000 --- a/customModules/submap/helpers.ts +++ /dev/null @@ -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): void => { - let submap = hyprland.message('submap'); - - const newLineCarriage = /\n/g; - submap = submap.replace(newLineCarriage, ''); - - if (submap === 'unknown request') { - submap = 'default'; - } - - submapStatus.value = submap; -}; diff --git a/customModules/submap/index.ts b/customModules/submap/index.ts deleted file mode 100644 index c83316e27..000000000 --- a/customModules/submap/index.ts +++ /dev/null @@ -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 = 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) => { - inputHandler(self, { - onPrimaryClick: { - cmd: leftClick, - }, - onSecondaryClick: { - cmd: rightClick, - }, - onMiddleClick: { - cmd: middleClick, - }, - onScrollUp: { - cmd: scrollUp, - }, - onScrollDown: { - cmd: scrollDown, - }, - }); - }, - }, - }); - - return submapModule; -}; diff --git a/customModules/theme.ts b/customModules/theme.ts deleted file mode 100644 index 7df775554..000000000 --- a/customModules/theme.ts +++ /dev/null @@ -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 => { - 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' }), - ], - }), - }); -}; diff --git a/customModules/utils.ts b/customModules/utils.ts deleted file mode 100644 index 426a24123..000000000 --- a/customModules/utils.ts +++ /dev/null @@ -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): void => { - if (postInputUpdater !== undefined) { - postInputUpdater.value = !postInputUpdater.value; - } -}; - -export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn, postInputUpdater?: VariableType): 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(func: T, limit: number): T { - let inThrottle: boolean; - return function (this: ThisParameterType, ...args: Parameters) { - 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) => { - runAsyncCommand(cmd, events, fn, postInputUpdater); - }, - 200 / interval, - ); - -const dummyVar = Variable(''); - -export const inputHandler = ( - self: Button, - { onPrimaryClick, onSecondaryClick, onMiddleClick, onScrollUp, onScrollDown }: InputHandlerEvents, - postInputUpdater?: VariableType, -): void => { - const sanitizeInput = (input: VariableType): 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, event: Gdk.Event): void => { - runAsyncCommand( - sanitizeInput(onPrimaryClick?.cmd || dummyVar), - { clicked, event }, - onPrimaryClick.fn, - postInputUpdater, - ); - }; - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand( - sanitizeInput(onSecondaryClick?.cmd || dummyVar), - { clicked, event }, - onSecondaryClick.fn, - postInputUpdater, - ); - }; - - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand( - sanitizeInput(onMiddleClick?.cmd || dummyVar), - { clicked, event }, - onMiddleClick.fn, - postInputUpdater, - ); - }; - - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler( - sanitizeInput(onScrollUp?.cmd || dummyVar), - { clicked, event }, - onScrollUp.fn, - postInputUpdater, - ); - }; - - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler( - sanitizeInput(onScrollDown?.cmd || dummyVar), - { clicked, event }, - onScrollDown.fn, - postInputUpdater, - ); - }; - }; - - // Initial setup of event handlers - updateHandlers(); - - const sanitizeVariable = (someVar: VariableType | undefined): Binding => { - 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 ''; - } -}; diff --git a/customModules/weather/index.ts b/customModules/weather/index.ts deleted file mode 100644 index b40995c29..000000000 --- a/customModules/weather/index.ts +++ /dev/null @@ -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) => { - inputHandler(self, { - onPrimaryClick: { - cmd: leftClick, - }, - onSecondaryClick: { - cmd: rightClick, - }, - onMiddleClick: { - cmd: middleClick, - }, - onScrollUp: { - cmd: scrollUp, - }, - onScrollDown: { - cmd: scrollDown, - }, - }); - }, - }, - }); - - return weatherModule; -}; diff --git a/directoryMonitorService.ts b/directoryMonitorService.ts deleted file mode 100644 index 2e1a74a9a..000000000 --- a/directoryMonitorService.ts +++ /dev/null @@ -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; diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 000000000..c9f063b35 --- /dev/null +++ b/env.d.ts @@ -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; +} diff --git a/external/ags-types b/external/ags-types deleted file mode 160000 index 87b504679..000000000 --- a/external/ags-types +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 87b5046791040615cd65b48a04be062662a46e36 diff --git a/globals/dropdown.ts b/globals/dropdown.ts deleted file mode 100644 index b1178bfd9..000000000 --- a/globals/dropdown.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Variable as VariableType } from 'types/variable'; - -type GlobalEventBoxes = { - [key: string]: unknown; -}; -export const globalEventBoxes: VariableType = Variable({}); diff --git a/globals/network.ts b/globals/network.ts deleted file mode 100644 index 44fbf7798..000000000 --- a/globals/network.ts +++ /dev/null @@ -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; diff --git a/globals/notification.ts b/globals/notification.ts deleted file mode 100644 index 9019f5f1b..000000000 --- a/globals/notification.ts +++ /dev/null @@ -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(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 => { - removingNotifications.value = true; - for (const notif of notifications) { - notif.close(); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - removingNotifications.value = false; -}; - -const clearAllNotifications = async (): Promise => { - clearNotifications(notifs.notifications, clearDelay.value); -}; - -globalThis['removingNotifications'] = removingNotifications; -globalThis['clearAllNotifications'] = clearAllNotifications; diff --git a/globals/systray.ts b/globals/systray.ts deleted file mode 100644 index 370c188f5..000000000 --- a/globals/systray.ts +++ /dev/null @@ -1,7 +0,0 @@ -const systemtray = await Service.import('systemtray'); - -globalThis.getSystrayItems = (): string => { - return systemtray.items.map((systrayItem) => systrayItem.id).join('\n'); -}; - -export { getSystrayItems }; diff --git a/globals/useTheme.ts b/globals/useTheme.ts deleted file mode 100644 index 83c61236d..000000000 --- a/globals/useTheme.ts +++ /dev/null @@ -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); -}; diff --git a/globals/utilities.ts b/globals/utilities.ts deleted file mode 100644 index cf767e14e..000000000 --- a/globals/utilities.ts +++ /dev/null @@ -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}`; - } -}; diff --git a/globals/wallpaper.ts b/globals/wallpaper.ts deleted file mode 100644 index 5ffb85c8a..000000000 --- a/globals/wallpaper.ts +++ /dev/null @@ -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); - } -}; diff --git a/lib/option.ts b/lib/option.ts deleted file mode 100644 index c26165767..000000000 --- a/lib/option.ts +++ /dev/null @@ -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 extends Variable { - 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 = (initial: T, opts?: OptProps): Opt => new Opt(initial, opts); - -const getOptions = (object: Record, 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, id); // Recursively process nested objects - } - - return []; - }); -}; - -export function mkOptions( - cacheFile: string, - object: T, - confFile: string = 'config.json', -): T & MkOptionsResult { - for (const opt of getOptions(object as Record)) opt.init(cacheFile); - - Utils.ensureDirectory(cacheFile.split('/').slice(0, -1).join('/')); - - const configFile = `${TMP}/${confFile}`; - const values = getOptions(object as Record).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)) { - if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value)) opt.value = cache[opt.id]; - } - }); - - function sleep(ms = 0): Promise { - return new Promise((r) => setTimeout(r, ms)); - } - - const reset = async ( - [opt, ...list] = getOptions(object as Record), - id = opt?.reset(), - ): Promise> => { - 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), - id = opt?.doResetColor(), - ): Promise> => { - 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), - 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)) { - if (deps.some((i) => opt.id.startsWith(i))) opt.connect('changed', callback); - } - }, - }); -} diff --git a/lib/session.ts b/lib/session.ts deleted file mode 100644 index 595b36602..000000000 --- a/lib/session.ts +++ /dev/null @@ -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`); diff --git a/lib/shared/media.ts b/lib/shared/media.ts deleted file mode 100644 index 23e0c6f04..000000000 --- a/lib/shared/media.ts +++ /dev/null @@ -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; -}; diff --git a/lib/shared/notifications.ts b/lib/shared/notifications.ts deleted file mode 100644 index f772c69e7..000000000 --- a/lib/shared/notifications.ts +++ /dev/null @@ -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; -}; diff --git a/lib/types/defaults/bar.ts b/lib/types/defaults/bar.ts deleted file mode 100644 index 78966c473..000000000 --- a/lib/types/defaults/bar.ts +++ /dev/null @@ -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']; diff --git a/lib/types/dropdownmenu.d.ts b/lib/types/dropdownmenu.d.ts deleted file mode 100644 index 8d76e5ca3..000000000 --- a/lib/types/dropdownmenu.d.ts +++ /dev/null @@ -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; - exclusivity?: Exclusivity; - fixed?: boolean; -} & WindowProps; diff --git a/lib/types/mpris.d.ts b/lib/types/mpris.d.ts deleted file mode 100644 index 5450e8083..000000000 --- a/lib/types/mpris.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type LoopStatus = 'none' | 'track' | 'playlist'; -export type PlaybackStatus = 'playing' | 'paused' | 'stopped'; diff --git a/lib/types/widget.d.ts b/lib/types/widget.d.ts deleted file mode 100644 index ebb09a48b..000000000 --- a/lib/types/widget.d.ts +++ /dev/null @@ -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; - -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, event: Gdk.Event) => boolean | unknown; -export type EventArgs = { clicked: Button; event: Gdk.Event }; diff --git a/lib/utils.ts b/lib/utils.ts deleted file mode 100644 index cdb8128dd..000000000 --- a/lib/utils.ts +++ /dev/null @@ -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 = import('types/service').Binding; - -/** - * 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 { - 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 { - 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, - }); - } - }); -}; diff --git a/lib/variables.ts b/lib/variables.ts deleted file mode 100644 index 303c12171..000000000 --- a/lib/variables.ts +++ /dev/null @@ -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'), -}; diff --git a/main.ts b/main.ts deleted file mode 100644 index efd5185f1..000000000 --- a/main.ts +++ /dev/null @@ -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); - } -}); diff --git a/make_agsv1.sh b/make_agsv1.sh deleted file mode 100755 index 365fbc954..000000000 --- a/make_agsv1.sh +++ /dev/null @@ -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." diff --git a/meson.build b/meson.build new file mode 100644 index 000000000..c8476db36 --- /dev/null +++ b/meson.build @@ -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') diff --git a/modules/bar/Bar.ts b/modules/bar/Bar.ts deleted file mode 100644 index d1751f92e..000000000 --- a/modules/bar/Bar.ts +++ /dev/null @@ -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 => WidgetContainer(BatteryLabel()), - dashboard: (): Button => WidgetContainer(Menu()), - workspaces: (monitor: number): Button => WidgetContainer(Workspaces(monitor)), - windowtitle: (): Button => WidgetContainer(ClientTitle()), - media: (): Button => WidgetContainer(Media()), - notifications: (): Button => WidgetContainer(Notifications()), - volume: (): Button => WidgetContainer(Volume()), - network: (): Button => WidgetContainer(Network()), - bluetooth: (): Button => WidgetContainer(Bluetooth()), - clock: (): Button => WidgetContainer(Clock()), - systray: (): Button => WidgetContainer(SysTray()), - ram: (): Button => WidgetContainer(Ram()), - cpu: (): Button => WidgetContainer(Cpu()), - cputemp: (): Button => WidgetContainer(CpuTemp()), - storage: (): Button => WidgetContainer(Storage()), - netstat: (): Button => WidgetContainer(Netstat()), - kbinput: (): Button => WidgetContainer(KbInput()), - updates: (): Button => WidgetContainer(Updates()), - submap: (): Button => WidgetContainer(Submap()), - weather: (): Button => WidgetContainer(Weather()), - power: (): Button => WidgetContainer(Power()), - hyprsunset: (): Button => WidgetContainer(Hyprsunset()), - hypridle: (): Button => 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 => { - 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(); - - return (monitor: number): Window => { - 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); - }); - }, - }), - 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); - }); - }, - }), - 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); - }); - }, - }), - }), - }), - }); - }; -})(); diff --git a/modules/bar/Exports.ts b/modules/bar/Exports.ts deleted file mode 100644 index 03229c424..000000000 --- a/modules/bar/Exports.ts +++ /dev/null @@ -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, -}; diff --git a/modules/bar/SideEffects.ts b/modules/bar/SideEffects.ts deleted file mode 100644 index e4aabecf6..000000000 --- a/modules/bar/SideEffects.ts +++ /dev/null @@ -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; - } -}); diff --git a/modules/bar/battery/index.ts b/modules/bar/battery/index.ts deleted file mode 100644 index 9a9b4f986..000000000 --- a/modules/bar/battery/index.ts +++ /dev/null @@ -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 => { - 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): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - onPrimaryClick: (clicked: Button, event: Gdk.Event): void => { - openMenu(clicked, event, 'energymenu'); - }, - }, - }; -}; - -export { BatteryLabel }; diff --git a/modules/bar/bluetooth/index.ts b/modules/bar/bluetooth/index.ts deleted file mode 100644 index 55381a304..000000000 --- a/modules/bar/bluetooth/index.ts +++ /dev/null @@ -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): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - on_primary_click: (clicked: Button, event: Gdk.Event): void => { - openMenu(clicked, event, 'bluetoothmenu'); - }, - }, - }; -}; - -export { Bluetooth }; diff --git a/modules/bar/clock/index.ts b/modules/bar/clock/index.ts deleted file mode 100644 index 0b2fe21af..000000000 --- a/modules/bar/clock/index.ts +++ /dev/null @@ -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): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - on_primary_click: (clicked: Button, event: Gdk.Event): void => { - openMenu(clicked, event, 'calendarmenu'); - }, - }, - }; -}; - -export { Clock }; diff --git a/modules/bar/media/helpers.ts b/modules/bar/media/helpers.ts deleted file mode 100644 index 04ffcf606..000000000 --- a/modules/bar/media/helpers.ts +++ /dev/null @@ -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, - show_label: Opt, - format: Opt, - songIcon: Variable, - activePlayer: Variable, -): 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`; - } -}; diff --git a/modules/bar/media/index.ts b/modules/bar/media/index.ts deleted file mode 100644 index 868989da9..000000000 --- a/modules/bar/media/index.ts +++ /dev/null @@ -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, event: Gdk.Event): void => { - openMenu(clicked, event, 'mediamenu'); - }, - onSecondaryClick: (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }, - onMiddleClick: (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }, - }, - }; -}; - -export { Media }; diff --git a/modules/bar/menu/index.ts b/modules/bar/menu/index.ts deleted file mode 100644 index 00ad72f8f..000000000 --- a/modules/bar/menu/index.ts +++ /dev/null @@ -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, event: Gdk.Event): void => { - openMenu(clicked, event, 'dashboardmenu'); - }, - setup: (self: Button): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - }, - }; -}; - -export { Menu }; diff --git a/modules/bar/network/index.ts b/modules/bar/network/index.ts deleted file mode 100644 index 32debe96e..000000000 --- a/modules/bar/network/index.ts +++ /dev/null @@ -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, event: Gdk.Event): void => { - openMenu(clicked, event, 'networkmenu'); - }, - setup: (self: Button): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - }, - }; -}; - -export { Network }; diff --git a/modules/bar/notifications/index.ts b/modules/bar/notifications/index.ts deleted file mode 100644 index f0e721d5d..000000000 --- a/modules/bar/notifications/index.ts +++ /dev/null @@ -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, event: Gdk.Event): void => { - openMenu(clicked, event, 'notificationsmenu'); - }, - setup: (self: Button): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - }, - }; -}; diff --git a/modules/bar/systray/index.ts b/modules/bar/systray/index.ts deleted file mode 100644 index 237c0dbec..000000000 --- a/modules/bar/systray/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/bar/volume/index.ts b/modules/bar/volume/index.ts deleted file mode 100644 index 07d8398e7..000000000 --- a/modules/bar/volume/index.ts +++ /dev/null @@ -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 = 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, event: Gdk.Event): void => { - openMenu(clicked, event, 'audiomenu'); - }, - setup: (self: Button): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - }, - }; -}; - -export { Volume }; diff --git a/modules/bar/window_title/index.ts b/modules/bar/window_title/index.ts deleted file mode 100644 index 6baf7644b..000000000 --- a/modules/bar/window_title/index.ts +++ /dev/null @@ -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 => { - 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[] = []; - 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): void => { - self.hook(options.bar.scrollSpeed, () => { - const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value); - - self.on_primary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(leftClick.value, { clicked, event }); - }; - self.on_secondary_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(rightClick.value, { clicked, event }); - }; - self.on_middle_click = (clicked: Button, event: Gdk.Event): void => { - runAsyncCommand(middleClick.value, { clicked, event }); - }; - self.on_scroll_up = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollUp.value, { clicked, event }); - }; - self.on_scroll_down = (clicked: Button, event: Gdk.Event): void => { - throttledHandler(scrollDown.value, { clicked, event }); - }; - }); - }, - }, - }; -}; - -export { ClientTitle }; diff --git a/modules/bar/workspaces/helpers.ts b/modules/bar/workspaces/helpers.ts deleted file mode 100644 index daf3cb32b..000000000 --- a/modules/bar/workspaces/helpers.ts +++ /dev/null @@ -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, 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, - activeWorkspaces: boolean, - ignoredWorkspaces: Variable, -): 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, - activeWorkspaces: boolean, - ignoredWorkspaces: Variable, -): void => { - navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces); -}; - -export const goToPrevWS = ( - currentMonitorWorkspaces: Variable, - activeWorkspaces: boolean, - ignoredWorkspaces: Variable, -): void => { - navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces); -}; - -export function throttle void>(func: T, limit: number): T { - let inThrottle: boolean; - return function (this: ThisParameterType, ...args: Parameters) { - if (!inThrottle) { - func.apply(this, args); - inThrottle = true; - setTimeout(() => { - inThrottle = false; - }, limit); - } - } as T; -} - -export const createThrottledScrollHandlers = ( - scrollSpeed: number, - currentMonitorWorkspaces: Variable, - 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 }; -}; diff --git a/modules/bar/workspaces/index.ts b/modules/bar/workspaces/index.ts deleted file mode 100644 index c95e2123f..000000000 --- a/modules/bar/workspaces/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/bar/workspaces/utils.ts b/modules/bar/workspaces/utils.ts deleted file mode 100644 index 3b99ad37f..000000000 --- a/modules/bar/workspaces/utils.ts +++ /dev/null @@ -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 = 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}`; -}; diff --git a/modules/bar/workspaces/variants/default.ts b/modules/bar/workspaces/variants/default.ts deleted file mode 100644 index b1938bef1..000000000 --- a/modules/bar/workspaces/variants/default.ts +++ /dev/null @@ -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); - }); - }, - }), - }); - }); - }, - ), - }); -}; diff --git a/modules/bar/workspaces/variants/occupied.ts b/modules/bar/workspaces/variants/occupied.ts deleted file mode 100644 index 49825f65f..000000000 --- a/modules/bar/workspaces/variants/occupied.ts +++ /dev/null @@ -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; - }, - ), - }); -}; diff --git a/modules/menus/audio/active/SelectedInput.ts b/modules/menus/audio/active/SelectedInput.ts deleted file mode 100644 index 4e3cff33d..000000000 --- a/modules/menus/audio/active/SelectedInput.ts +++ /dev/null @@ -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[] => { - 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 }; diff --git a/modules/menus/audio/active/SelectedPlayback.ts b/modules/menus/audio/active/SelectedPlayback.ts deleted file mode 100644 index ef1bb5c9a..000000000 --- a/modules/menus/audio/active/SelectedPlayback.ts +++ /dev/null @@ -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[] => { - 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 }; diff --git a/modules/menus/audio/active/index.ts b/modules/menus/audio/active/index.ts deleted file mode 100644 index 9768ddb8a..000000000 --- a/modules/menus/audio/active/index.ts +++ /dev/null @@ -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 => { - 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 }; diff --git a/modules/menus/audio/available/InputDevices.ts b/modules/menus/audio/available/InputDevices.ts deleted file mode 100644 index 8c75a5acd..000000000 --- a/modules/menus/audio/available/InputDevices.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/audio/available/PlaybackDevices.ts b/modules/menus/audio/available/PlaybackDevices.ts deleted file mode 100644 index 02bc618b4..000000000 --- a/modules/menus/audio/available/PlaybackDevices.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/audio/available/index.ts b/modules/menus/audio/available/index.ts deleted file mode 100644 index e9b0d4894..000000000 --- a/modules/menus/audio/available/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/audio/index.ts b/modules/menus/audio/index.ts deleted file mode 100644 index 680115192..000000000 --- a/modules/menus/audio/index.ts +++ /dev/null @@ -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 => { - 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()], - }), - }), - }); -}; diff --git a/modules/menus/bluetooth/devices/connectedControls.ts b/modules/menus/bluetooth/devices/connectedControls.ts deleted file mode 100644 index 62e8345b7..000000000 --- a/modules/menus/bluetooth/devices/connectedControls.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/bluetooth/devices/devicelist.ts b/modules/menus/bluetooth/devices/devicelist.ts deleted file mode 100644 index af1a66b72..000000000 --- a/modules/menus/bluetooth/devices/devicelist.ts +++ /dev/null @@ -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): Box => { - 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 => { - 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 }; diff --git a/modules/menus/bluetooth/devices/index.ts b/modules/menus/bluetooth/devices/index.ts deleted file mode 100644 index 31315d5c7..000000000 --- a/modules/menus/bluetooth/devices/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/bluetooth/devices/label.ts b/modules/menus/bluetooth/devices/label.ts deleted file mode 100644 index 45fabf62d..000000000 --- a/modules/menus/bluetooth/devices/label.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/bluetooth/index.ts b/modules/menus/bluetooth/index.ts deleted file mode 100644 index 54dfcad66..000000000 --- a/modules/menus/bluetooth/index.ts +++ /dev/null @@ -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 => { - 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(), - }), - }), - }); -}; diff --git a/modules/menus/calendar/calendar.ts b/modules/menus/calendar/calendar.ts deleted file mode 100644 index d2af59ecb..000000000 --- a/modules/menus/calendar/calendar.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/calendar/index.ts b/modules/menus/calendar/index.ts deleted file mode 100644 index d3fecb8e8..000000000 --- a/modules/menus/calendar/index.ts +++ /dev/null @@ -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 => { - 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()] : [])]; - }), - }), - ], - }), - ], - }), - }); -}; diff --git a/modules/menus/calendar/time/index.ts b/modules/menus/calendar/time/index.ts deleted file mode 100644 index 2cbb2795b..000000000 --- a/modules/menus/calendar/time/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/calendar/weather/hourly/icon/index.ts b/modules/menus/calendar/weather/hourly/icon/index.ts deleted file mode 100644 index 2c636c1cc..000000000 --- a/modules/menus/calendar/weather/hourly/icon/index.ts +++ /dev/null @@ -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, 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, - }); - }), - }); -}; diff --git a/modules/menus/calendar/weather/hourly/index.ts b/modules/menus/calendar/weather/hourly/index.ts deleted file mode 100644 index 0eb1982bd..000000000 --- a/modules/menus/calendar/weather/hourly/index.ts +++ /dev/null @@ -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): 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), - ], - }); - }), - }); -}; diff --git a/modules/menus/calendar/weather/hourly/temperature/index.ts b/modules/menus/calendar/weather/hourly/temperature/index.ts deleted file mode 100644 index 440c994a2..000000000 --- a/modules/menus/calendar/weather/hourly/temperature/index.ts +++ /dev/null @@ -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, hoursFromNow: number): Label => { - 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`; - }), - }); -}; diff --git a/modules/menus/calendar/weather/hourly/time/index.ts b/modules/menus/calendar/weather/hourly/time/index.ts deleted file mode 100644 index 717eac4b7..000000000 --- a/modules/menus/calendar/weather/hourly/time/index.ts +++ /dev/null @@ -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, hoursFromNow: number): Label => { - 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}`; - }), - }); -}; diff --git a/modules/menus/calendar/weather/hourly/utils.ts b/modules/menus/calendar/weather/hourly/utils.ts deleted file mode 100644 index 13f1d53b7..000000000 --- a/modules/menus/calendar/weather/hourly/utils.ts +++ /dev/null @@ -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; -}; diff --git a/modules/menus/calendar/weather/icon/index.ts b/modules/menus/calendar/weather/icon/index.ts deleted file mode 100644 index dff981a3d..000000000 --- a/modules/menus/calendar/weather/icon/index.ts +++ /dev/null @@ -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): 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); - }), - }), - }); -}; diff --git a/modules/menus/calendar/weather/index.ts b/modules/menus/calendar/weather/index.ts deleted file mode 100644 index bf4a6bfd5..000000000 --- a/modules/menus/calendar/weather/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/calendar/weather/stats/index.ts b/modules/menus/calendar/weather/stats/index.ts deleted file mode 100644 index 197032f2f..000000000 --- a/modules/menus/calendar/weather/stats/index.ts +++ /dev/null @@ -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): 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)), - }), - ], - }), - ], - }); -}; diff --git a/modules/menus/calendar/weather/temperature/index.ts b/modules/menus/calendar/weather/temperature/index.ts deleted file mode 100644 index 6a874eb66..000000000 --- a/modules/menus/calendar/weather/temperature/index.ts +++ /dev/null @@ -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): 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), - }), - }), - ], - }); -}; diff --git a/modules/menus/dashboard/controls/index.ts b/modules/menus/dashboard/controls/index.ts deleted file mode 100644 index e158f96c5..000000000 --- a/modules/menus/dashboard/controls/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/dashboard/directories/index.ts b/modules/menus/dashboard/directories/index.ts deleted file mode 100644 index 5b6aa11ae..000000000 --- a/modules/menus/dashboard/directories/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/dashboard/index.ts b/modules/menus/dashboard/index.ts deleted file mode 100644 index 9bac607f8..000000000 --- a/modules/menus/dashboard/index.ts +++ /dev/null @@ -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 => { - 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()] : []), - ], - }), - ]; - }, - ), - }), - ], - }), - }); -}; diff --git a/modules/menus/dashboard/profile/index.ts b/modules/menus/dashboard/profile/index.ts deleted file mode 100644 index 9d2531a6c..000000000 --- a/modules/menus/dashboard/profile/index.ts +++ /dev/null @@ -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 => { - 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 }; diff --git a/modules/menus/dashboard/shortcuts/index.ts b/modules/menus/dashboard/shortcuts/index.ts deleted file mode 100644 index a0d294f66..000000000 --- a/modules/menus/dashboard/shortcuts/index.ts +++ /dev/null @@ -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( - 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; - command: VarType; - icon: VarType; - 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, 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, Attribute> | Box => { - 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 }; diff --git a/modules/menus/dashboard/stats/index.ts b/modules/menus/dashboard/stats/index.ts deleted file mode 100644 index 542a23f69..000000000 --- a/modules/menus/dashboard/stats/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/energy/brightness/index.ts b/modules/menus/energy/brightness/index.ts deleted file mode 100644 index cdefd1a8f..000000000 --- a/modules/menus/energy/brightness/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/energy/index.ts b/modules/menus/energy/index.ts deleted file mode 100644 index 134d2cc5d..000000000 --- a/modules/menus/energy/index.ts +++ /dev/null @@ -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 => { - 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()], - }), - }), - }); -}; diff --git a/modules/menus/energy/profiles/index.ts b/modules/menus/energy/profiles/index.ts deleted file mode 100644 index 72b28242d..000000000 --- a/modules/menus/energy/profiles/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/media/components/controls/index.ts b/modules/menus/media/components/controls/index.ts deleted file mode 100644 index 79dbf0e70..000000000 --- a/modules/menus/media/components/controls/index.ts +++ /dev/null @@ -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 }; diff --git a/modules/menus/media/components/controls/loop/helpers.ts b/modules/menus/media/components/controls/loop/helpers.ts deleted file mode 100644 index 2a8d9a205..000000000 --- a/modules/menus/media/components/controls/loop/helpers.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { LoopStatus } from 'lib/types/mpris'; -import { MprisPlayer } from 'types/service/mpris'; - -export const isValidLoopStatus = (status: string): status is LoopStatus => - ['none', 'track', 'playlist'].includes(status); - -export const isLoopActive = (player: MprisPlayer): string => { - return player['loop_status'] !== null && ['track', 'playlist'].includes(player['loop_status'].toLowerCase()) - ? 'active' - : ''; -}; diff --git a/modules/menus/media/components/controls/loop/index.ts b/modules/menus/media/components/controls/loop/index.ts deleted file mode 100644 index cfd94c0eb..000000000 --- a/modules/menus/media/components/controls/loop/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import icons from 'lib/icons'; -import { BoxWidget } from 'lib/types/widget'; -import { getPlayerInfo } from '../../helpers'; -import { isLoopActive, isValidLoopStatus } from './helpers'; - -const media = await Service.import('mpris'); - -export const loopControl = (): BoxWidget => { - return Widget.Box({ - class_name: 'media-indicator-control loop', - children: [ - Widget.Button({ - hpack: 'center', - setup: (self) => { - self.hook(media, () => { - const foundPlayer = getPlayerInfo(); - if (foundPlayer === undefined) { - self.tooltip_text = 'Unavailable'; - self.class_name = 'media-indicator-control-button shuffle disabled'; - return; - } - - self.tooltip_text = foundPlayer.loop_status !== null ? foundPlayer.loop_status : 'None'; - - self.on_primary_click = (): void => { - foundPlayer.loop(); - }; - - const statusTag = foundPlayer.loop_status !== null ? 'enabled' : 'disabled'; - const isActiveTag = isLoopActive(foundPlayer); - - self.class_name = `media-indicator-control-button loop ${isActiveTag} ${statusTag}`; - }); - }, - child: Widget.Icon({ - setup: (self) => { - self.hook(media, () => { - const foundPlayer = getPlayerInfo(); - - if (foundPlayer === undefined) { - self.icon = icons.mpris.loop['none']; - return; - } - - const loopStatus = foundPlayer.loop_status?.toLowerCase(); - - if (loopStatus && isValidLoopStatus(loopStatus)) { - self.icon = icons.mpris.loop[loopStatus]; - } else { - self.icon = icons.mpris.loop['none']; - } - }); - }, - }), - }), - ], - }); -}; diff --git a/modules/menus/media/components/controls/next/index.ts b/modules/menus/media/components/controls/next/index.ts deleted file mode 100644 index aae4c33b7..000000000 --- a/modules/menus/media/components/controls/next/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -const media = await Service.import('mpris'); -import icons from 'lib/icons'; -import { BoxWidget } from 'lib/types/widget'; -import { getPlayerInfo } from '../../helpers'; - -export const nextTrack = (): BoxWidget => { - return Widget.Box({ - class_name: `media-indicator-control next`, - children: [ - Widget.Button({ - hpack: 'center', - child: Widget.Icon(icons.mpris.next), - setup: (self) => { - self.hook(media, () => { - const foundPlayer = getPlayerInfo(); - if (foundPlayer === undefined) { - self.class_name = 'media-indicator-control-button next disabled'; - return; - } - - self.on_primary_click = (): void => { - foundPlayer.next(); - }; - self.class_name = `media-indicator-control-button next ${foundPlayer.can_go_next !== null && foundPlayer.can_go_next ? 'enabled' : 'disabled'}`; - }); - }, - }), - ], - }); -}; diff --git a/modules/menus/media/components/controls/playpause/helpers.ts b/modules/menus/media/components/controls/playpause/helpers.ts deleted file mode 100644 index fa8b0d188..000000000 --- a/modules/menus/media/components/controls/playpause/helpers.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PlaybackStatus } from 'lib/types/mpris'; - -export const isValidPlaybackStatus = (status: string): status is PlaybackStatus => - ['playing', 'paused', 'stopped'].includes(status); diff --git a/modules/menus/media/components/controls/playpause/index.ts b/modules/menus/media/components/controls/playpause/index.ts deleted file mode 100644 index f931eb35c..000000000 --- a/modules/menus/media/components/controls/playpause/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -const media = await Service.import('mpris'); -import { getPlayerInfo } from '../../helpers'; -import { isValidPlaybackStatus } from './helpers'; -import Button from 'types/widgets/button'; -import Icon from 'types/widgets/icon'; -import { Attribute } from 'lib/types/widget'; -import icons from 'modules/icons/index'; - -export const playPause = (): Button, Attribute> => { - return Widget.Button({ - hpack: 'center', - setup: (self) => { - self.hook(media, () => { - const foundPlayer = getPlayerInfo(); - if (foundPlayer === undefined) { - self.class_name = 'media-indicator-control-button play disabled'; - return; - } - - self.on_primary_click = (): void => { - foundPlayer.playPause(); - }; - self.class_name = `media-indicator-control-button play ${foundPlayer.can_play !== null ? 'enabled' : 'disabled'}`; - }); - }, - child: Widget.Icon({ - icon: Utils.watch(icons.mpris.paused, media, 'changed', () => { - const foundPlayer = getPlayerInfo(); - - if (foundPlayer === undefined) { - return icons.mpris['paused']; - } - - const playbackStatus = foundPlayer.play_back_status?.toLowerCase(); - - if (playbackStatus && isValidPlaybackStatus(playbackStatus)) { - return icons.mpris[playbackStatus]; - } else { - return icons.mpris['paused']; - } - }), - }), - }); -}; diff --git a/modules/menus/media/components/controls/previous/index.ts b/modules/menus/media/components/controls/previous/index.ts deleted file mode 100644 index b5094ede2..000000000 --- a/modules/menus/media/components/controls/previous/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -const media = await Service.import('mpris'); -import icons from 'lib/icons'; -import { Attribute } from 'lib/types/widget'; -import { getPlayerInfo } from '../../helpers'; -import Button from 'types/widgets/button'; -import Icon from 'types/widgets/icon'; - -export const previousTrack = (): Button, Attribute> => { - return Widget.Button({ - hpack: 'center', - child: Widget.Icon(icons.mpris.prev), - setup: (self) => { - self.hook(media, () => { - const foundPlayer = getPlayerInfo(); - if (foundPlayer === undefined) { - self.class_name = 'media-indicator-control-button prev disabled'; - return; - } - - self.on_primary_click = (): void => { - foundPlayer.previous(); - }; - self.class_name = `media-indicator-control-button prev ${foundPlayer.can_go_prev !== null && foundPlayer.can_go_prev ? 'enabled' : 'disabled'}`; - }); - }, - }); -}; diff --git a/modules/menus/media/components/controls/shuffle/helpers.ts b/modules/menus/media/components/controls/shuffle/helpers.ts deleted file mode 100644 index 9286dc0a8..000000000 --- a/modules/menus/media/components/controls/shuffle/helpers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { MprisPlayer } from 'types/service/mpris'; - -export const isShuffleActive = (player: MprisPlayer): string => { - return player['shuffle_status'] !== null && player['shuffle_status'] ? 'active' : ''; -}; diff --git a/modules/menus/media/components/controls/shuffle/index.ts b/modules/menus/media/components/controls/shuffle/index.ts deleted file mode 100644 index 50fb6413b..000000000 --- a/modules/menus/media/components/controls/shuffle/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -const media = await Service.import('mpris'); -import { Attribute, Child } from 'lib/types/widget'; -import { getPlayerInfo } from '../../helpers'; -import Box from 'types/widgets/box'; -import { isShuffleActive } from './helpers'; -import icons from 'lib/icons'; - -export const shuffleControl = (): Box => { - return Widget.Box({ - class_name: 'media-indicator-control shuffle', - children: [ - Widget.Button({ - hpack: 'center', - hasTooltip: true, - setup: (self) => { - self.hook(media, () => { - const foundPlayer = getPlayerInfo(); - if (foundPlayer === undefined) { - self.tooltip_text = 'Unavailable'; - self.class_name = 'media-indicator-control-button shuffle disabled'; - return; - } - - self.tooltip_text = - foundPlayer.shuffle_status !== null - ? foundPlayer.shuffle_status - ? 'Shuffling' - : 'Not Shuffling' - : null; - self.on_primary_click = (): void => { - foundPlayer.shuffle(); - }; - self.class_name = `media-indicator-control-button shuffle ${isShuffleActive(foundPlayer)} ${foundPlayer.shuffle_status !== null ? 'enabled' : 'disabled'}`; - }); - }, - child: Widget.Icon(icons.mpris.shuffle['enabled']), - }), - ], - }); -}; diff --git a/modules/menus/media/components/helpers.ts b/modules/menus/media/components/helpers.ts deleted file mode 100644 index 0d08a007f..000000000 --- a/modules/menus/media/components/helpers.ts +++ /dev/null @@ -1,54 +0,0 @@ -const media = await Service.import('mpris'); -import options from 'options.js'; -import { MprisPlayer } from 'types/service/mpris'; -const { tint, color } = options.theme.bar.menus.menu.media.card; - -const curPlayer = Variable(''); - -export const generateAlbumArt = (imageUrl: string): string => { - const userTint = tint.value; - const userHexColor = color.value; - - const r = parseInt(userHexColor.slice(1, 3), 16); - const g = parseInt(userHexColor.slice(3, 5), 16); - const b = parseInt(userHexColor.slice(5, 7), 16); - - const alpha = userTint / 100; - - const css = `background-image: linear-gradient( - rgba(${r}, ${g}, ${b}, ${alpha}), - rgba(${r}, ${g}, ${b}, ${alpha}), - ${userHexColor} 65em - ), url("${imageUrl}");`; - - return css; -}; - -export const initializeActivePlayerHook = (): void => { - media.connect('changed', () => { - const statusOrder = { - Playing: 1, - Paused: 2, - Stopped: 3, - }; - - const isPlaying = media.players.find((p) => p['play_back_status'] === 'Playing'); - - const playerStillExists = media.players.some((p) => curPlayer.value === p['bus_name']); - - const nextPlayerUp = media.players.sort( - (a, b) => statusOrder[a['play_back_status']] - statusOrder[b['play_back_status']], - )[0].bus_name; - - if (isPlaying || !playerStillExists) { - curPlayer.value = nextPlayerUp; - } - }); -}; - -export const getPlayerInfo = (): MprisPlayer | undefined => { - if (media.players.length === 0) { - return; - } - return media.players.find((p) => p.bus_name === curPlayer.value) || media.players[0]; -}; diff --git a/modules/menus/media/components/index.ts b/modules/menus/media/components/index.ts deleted file mode 100644 index dcee74128..000000000 --- a/modules/menus/media/components/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -const media = await Service.import('mpris'); -import { MediaInfo } from './title/index.js'; -import { Controls } from './controls/index.js'; -import { Bar } from './timebar/index.js'; -import options from 'options.js'; -import { BoxWidget } from 'lib/types/widget.js'; -import { generateAlbumArt, getPlayerInfo, initializeActivePlayerHook } from './helpers.js'; -import { Time } from './timelabel/index.js'; - -const { tint, color } = options.theme.bar.menus.menu.media.card; -const { displayTime } = options.menus.media; - -initializeActivePlayerHook(); - -const Media = (): BoxWidget => { - return Widget.Box({ - class_name: 'menu-section-container', - children: [ - Widget.Box({ - class_name: 'menu-items-section', - vertical: false, - child: Widget.Box({ - class_name: 'menu-content', - child: Widget.Box({ - class_name: 'media-content', - child: Widget.Box({ - class_name: 'media-indicator-right-section', - hpack: 'fill', - hexpand: true, - vertical: true, - children: displayTime.bind('value').as((showTime) => { - return [MediaInfo(), Controls(), Bar(), ...(showTime ? [Time()] : [])]; - }), - }), - }), - setup: (self) => { - self.hook(media, () => { - const curPlayer = getPlayerInfo(); - - if (curPlayer !== undefined) { - self.css = generateAlbumArt(curPlayer.track_cover_url); - } - }); - - Utils.merge([color.bind('value'), tint.bind('value')], () => { - const curPlayer = getPlayerInfo(); - if (curPlayer !== undefined) { - self.css = generateAlbumArt(curPlayer.track_cover_url); - } - }); - }, - }), - }), - ], - }); -}; - -export { Media }; diff --git a/modules/menus/media/components/timebar/helpers.ts b/modules/menus/media/components/timebar/helpers.ts deleted file mode 100644 index 22a915a08..000000000 --- a/modules/menus/media/components/timebar/helpers.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Attribute } from 'lib/types/widget'; -import { MprisPlayer } from 'types/service/mpris'; -import Label from 'types/widgets/label'; -import Slider from 'types/widgets/slider'; - -/** - * Updates the tooltip text of the slider based on the player's current position. - * - * @param self - The slider component to update. - * @param foundPlayer - The MPRIS player object, if available. - */ -export const updateTooltip = (self: Slider, foundPlayer?: MprisPlayer): void => { - if (foundPlayer === undefined) { - self.tooltip_text = '00:00'; - return; - } - - const playerPosition = foundPlayer.position; - - const mediaLength = foundPlayer.length; - - if (typeof foundPlayer.position === 'number' && foundPlayer.position >= 0) { - self.tooltip_text = `${getFormattedTime(playerPosition)} / ${getFormattedTime(mediaLength)}`; - } else { - self.tooltip_text = `00:00`; - } -}; - -/** - * Updates the label text of the timestamp based on the player's current position. - * - * @param self - The label component to update. - * @param foundPlayer - The MPRIS player object, if available. - */ -export const updateTimestamp = (self: Label, foundPlayer?: MprisPlayer): void => { - if (foundPlayer === undefined) { - self.label = '00:00'; - return; - } - - const playerPosition = foundPlayer.position; - - const mediaLength = foundPlayer.length; - - if (typeof foundPlayer.position === 'number' && foundPlayer.position >= 0) { - self.label = `${getFormattedTime(playerPosition)} / ${getFormattedTime(mediaLength)}`; - } else { - self.label = `00:00`; - } -}; - -/** - * Updates the value of the slider based on the player's current position and length. - * - * @param self - The slider component to update. - * @param foundPlayer - The MPRIS player object, if available. - */ -export const update = (self: Slider, foundPlayer?: MprisPlayer): void => { - if (foundPlayer !== undefined) { - const value = foundPlayer.length ? foundPlayer.position / foundPlayer.length : 0; - self.value = value > 0 ? value : 0; - } else { - self.value = 0; - } -}; - -export const getFormattedTime = (time: number): string => { - const curHour = Math.floor(time / 3600); - const curMin = Math.floor((time % 3600) / 60); - const curSec = Math.floor(time % 60); - - const formatTime = (time: number): string => { - return time.toString().padStart(2, '0'); - }; - - const formatHour = (hour: number): string => { - return hour > 0 ? formatTime(hour) + ':' : ''; - }; - - return `${formatHour(curHour)}${formatTime(curMin)}:${formatTime(curSec)}`; -}; diff --git a/modules/menus/media/components/timebar/index.ts b/modules/menus/media/components/timebar/index.ts deleted file mode 100644 index 50e416486..000000000 --- a/modules/menus/media/components/timebar/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -const media = await Service.import('mpris'); -import options from 'options'; -import { BoxWidget } from 'lib/types/widget'; -import { getPlayerInfo } from '../helpers'; -import { update, updateTooltip } from './helpers'; - -const { displayTimeTooltip } = options.menus.media; - -const Bar = (): BoxWidget => { - return Widget.Box({ - class_name: 'media-indicator-current-progress-bar', - hexpand: true, - children: [ - Widget.Box({ - hexpand: true, - child: Widget.Slider({ - hexpand: true, - tooltip_text: '--', - class_name: 'menu-slider media progress', - draw_value: false, - on_change: ({ value }) => { - const foundPlayer = getPlayerInfo(); - if (foundPlayer === undefined) { - return; - } - return (foundPlayer.position = value * foundPlayer.length); - }, - setup: (self) => { - self.poll(1000, () => { - const foundPlayer = getPlayerInfo(); - - if (foundPlayer?.play_back_status !== 'Playing') return; - - update(self, foundPlayer); - - if (!displayTimeTooltip.value) { - self.tooltip_text = ''; - return; - } - - updateTooltip(self, foundPlayer); - }); - - self.hook(media, () => { - const foundPlayer = getPlayerInfo(); - update(self, foundPlayer); - - if (!displayTimeTooltip.value) return; - - updateTooltip(self, foundPlayer); - }); - }, - }), - }), - ], - }); -}; - -export { Bar }; diff --git a/modules/menus/media/components/timelabel/index.ts b/modules/menus/media/components/timelabel/index.ts deleted file mode 100644 index f06605f55..000000000 --- a/modules/menus/media/components/timelabel/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BoxWidget } from 'lib/types/widget'; -import { getPlayerInfo } from '../helpers'; -import { updateTimestamp } from '../timebar/helpers'; - -const Time = (): BoxWidget => { - return Widget.Box({ - class_name: 'media-indicator-current-time-label', - hexpand: true, - children: [ - Widget.Box({ - hexpand: true, - child: Widget.Label({ - hexpand: true, - tooltip_text: '--', - class_name: 'time-label', - setup: (self) => { - self.poll(1000, () => { - const foundPlayer = getPlayerInfo(); - updateTimestamp(self, foundPlayer); - }); - }, - }), - }), - ], - }); -}; - -export { Time }; diff --git a/modules/menus/media/components/title/album/index.ts b/modules/menus/media/components/title/album/index.ts deleted file mode 100644 index 29a2be8c6..000000000 --- a/modules/menus/media/components/title/album/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -const media = await Service.import('mpris'); -import { BoxWidget } from 'lib/types/widget'; -import { getPlayerInfo } from '../../helpers'; - -export const songAlbum = (): BoxWidget => { - return Widget.Box({ - class_name: 'media-indicator-current-song-album', - hpack: 'center', - children: [ - Widget.Label({ - truncate: 'end', - wrap: true, - max_width_chars: 40, - class_name: 'media-indicator-current-song-album-label', - setup: (self) => { - self.hook(media, () => { - const curPlayer = getPlayerInfo(); - return (self.label = - curPlayer !== undefined && curPlayer['track_album'].length - ? curPlayer['track_album'] - : '---'); - }); - }, - }), - ], - }); -}; diff --git a/modules/menus/media/components/title/author/index.ts b/modules/menus/media/components/title/author/index.ts deleted file mode 100644 index 3e3d5afc9..000000000 --- a/modules/menus/media/components/title/author/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -const media = await Service.import('mpris'); -import { BoxWidget } from 'lib/types/widget'; -import { getPlayerInfo } from '../../helpers'; - -export const songAuthor = (): BoxWidget => { - return Widget.Box({ - class_name: 'media-indicator-current-song-author', - hpack: 'center', - children: [ - Widget.Label({ - truncate: 'end', - wrap: true, - max_width_chars: 35, - class_name: 'media-indicator-current-song-author-label', - setup: (self) => { - self.hook(media, () => { - const curPlayer = getPlayerInfo(); - - const makeArtistList = (trackArtists: string[]): string => { - if (trackArtists.length === 1 && !trackArtists[0].length) { - return '-----'; - } - - return trackArtists.join(', '); - }; - - return (self.label = - curPlayer !== undefined && curPlayer['track_artists'].length - ? makeArtistList(curPlayer['track_artists']) - : '-----'); - }); - }, - }), - ], - }); -}; diff --git a/modules/menus/media/components/title/index.ts b/modules/menus/media/components/title/index.ts deleted file mode 100644 index 5934e30e1..000000000 --- a/modules/menus/media/components/title/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BoxWidget } from 'lib/types/widget'; -import { songName } from './name/index'; -import { songAuthor } from './author/index'; -import { songAlbum } from './album/index'; -import options from 'options'; - -const { hideAlbum, hideAuthor } = options.menus.media; - -export const MediaInfo = (): BoxWidget => { - return Widget.Box({ - class_name: 'media-indicator-current-media-info', - hpack: 'center', - hexpand: true, - vertical: true, - children: Utils.merge([hideAlbum.bind('value'), hideAuthor.bind('value')], (hidAlbum, hidAuthor) => { - return [songName(), ...(hidAuthor ? [] : [songAuthor()]), ...(hidAlbum ? [] : [songAlbum()])]; - }), - }); -}; diff --git a/modules/menus/media/components/title/name/index.ts b/modules/menus/media/components/title/name/index.ts deleted file mode 100644 index 1f9b35590..000000000 --- a/modules/menus/media/components/title/name/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { BoxWidget } from 'lib/types/widget'; -import { getPlayerInfo } from '../../helpers'; -import options from 'options'; - -const media = await Service.import('mpris'); - -const { noMediaText } = options.menus.media; - -export const songName = (): BoxWidget => { - return Widget.Box({ - class_name: 'media-indicator-current-song-name', - hpack: 'center', - children: [ - Widget.Label({ - truncate: 'end', - max_width_chars: 31, - wrap: true, - class_name: 'media-indicator-current-song-name-label', - setup: (self) => { - return Utils.merge([noMediaText.bind('value')], (noMediaTxt) => { - self.hook(media, () => { - const curPlayer = getPlayerInfo(); - return (self.label = - curPlayer !== undefined && curPlayer['track_title'].length - ? curPlayer['track_title'] - : noMediaTxt); - }); - }); - }, - }), - ], - }); -}; diff --git a/modules/menus/media/index.ts b/modules/menus/media/index.ts deleted file mode 100644 index 24e4714b7..000000000 --- a/modules/menus/media/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Window from 'types/widgets/window.js'; -import DropdownMenu from '../shared/dropdown/index.js'; -import { Media } from './components/index.js'; -import { Attribute, Child } from 'lib/types/widget.js'; -import options from 'options.js'; - -export default (): Window => { - return DropdownMenu({ - name: 'mediamenu', - transition: options.menus.transition.bind('value'), - child: Widget.Box({ - class_name: 'menu-items media', - hpack: 'fill', - hexpand: true, - child: Widget.Box({ - class_name: 'menu-items-container media', - hpack: 'fill', - hexpand: true, - child: Media(), - }), - }), - }); -}; diff --git a/modules/menus/network/ethernet/index.ts b/modules/menus/network/ethernet/index.ts deleted file mode 100644 index 5fdbf9797..000000000 --- a/modules/menus/network/ethernet/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { BoxWidget } from 'lib/types/widget'; -import { capitalizeFirstLetter } from 'lib/utils'; - -const network = await Service.import('network'); - -const Ethernet = (): BoxWidget => { - return Widget.Box({ - class_name: 'menu-section-container ethernet', - vertical: true, - children: [ - Widget.Box({ - class_name: 'menu-label-container', - hpack: 'fill', - child: Widget.Label({ - class_name: 'menu-label', - hexpand: true, - hpack: 'start', - label: 'Ethernet', - }), - }), - Widget.Box({ - class_name: 'menu-items-section', - vertical: true, - child: Widget.Box({ - class_name: 'menu-content', - vertical: true, - setup: (self) => { - self.hook(network, () => { - return (self.child = Widget.Box({ - class_name: 'network-element-item', - child: Widget.Box({ - hpack: 'start', - children: [ - Widget.Icon({ - class_name: `network-icon ethernet ${network.wired.state === 'activated' ? 'active' : ''}`, - tooltip_text: network.wired.internet, - icon: `${network.wired['icon_name']}`, - }), - Widget.Box({ - class_name: 'connection-container', - vertical: true, - children: [ - Widget.Label({ - class_name: 'active-connection', - hpack: 'start', - truncate: 'end', - wrap: true, - label: `Ethernet Connection ${network.wired.state !== 'unknown' && typeof network.wired?.speed === 'number' ? `(${network.wired?.speed / 1000} Gbps)` : ''}`, - }), - Widget.Label({ - hpack: 'start', - class_name: 'connection-status dim', - label: capitalizeFirstLetter(network.wired.internet), - }), - ], - }), - ], - }), - })); - }); - }, - }), - }), - ], - }); -}; - -export { Ethernet }; diff --git a/modules/menus/network/index.ts b/modules/menus/network/index.ts deleted file mode 100644 index f3e88dcc0..000000000 --- a/modules/menus/network/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Window from 'types/widgets/window.js'; -import DropdownMenu from '../shared/dropdown/index.js'; -import { Ethernet } from './ethernet/index.js'; -import { Wifi } from './wifi/index.js'; -import { Attribute, Child } from 'lib/types/widget.js'; -import options from 'options.js'; - -export default (): Window => { - return DropdownMenu({ - name: 'networkmenu', - transition: options.menus.transition.bind('value'), - child: Widget.Box({ - class_name: 'menu-items network', - child: Widget.Box({ - vertical: true, - hexpand: true, - class_name: 'menu-items-container network', - children: [Ethernet(), Wifi()], - }), - }), - }); -}; diff --git a/modules/menus/network/wifi/APStaging.ts b/modules/menus/network/wifi/APStaging.ts deleted file mode 100644 index 29b2798b8..000000000 --- a/modules/menus/network/wifi/APStaging.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Network } from 'types/service/network'; -import { Variable } from 'types/variable'; -import { AccessPoint } from 'lib/types/network'; -import Box from 'types/widgets/box'; -import { Attribute, Child } from 'lib/types/widget'; - -const renderWapStaging = ( - self: Box, - network: Network, - staging: Variable, - connecting: Variable, -): void => { - Utils.merge([network.bind('wifi'), staging.bind('value')], () => { - if (!Object.keys(staging.value).length) { - return (self.child = Widget.Box()); - } - - return (self.child = Widget.Box({ - class_name: 'network-element-item staging', - vertical: true, - children: [ - Widget.Box({ - hpack: 'fill', - hexpand: true, - children: [ - Widget.Icon({ - class_name: `network-icon wifi`, - icon: `${staging.value.iconName}`, - }), - Widget.Box({ - class_name: 'connection-container', - hexpand: true, - vertical: true, - children: [ - Widget.Label({ - class_name: 'active-connection', - hpack: 'start', - truncate: 'end', - wrap: true, - label: staging.value.ssid, - }), - ], - }), - Widget.Revealer({ - hpack: 'end', - reveal_child: connecting.bind('value').as((c) => staging.value.bssid === c), - child: Widget.Spinner({ - class_name: 'spinner wap', - }), - }), - ], - }), - Widget.Box({ - class_name: 'network-password-input-container', - hpack: 'fill', - hexpand: true, - children: [ - Widget.Entry({ - hpack: 'start', - hexpand: true, - visibility: false, - class_name: 'network-password-input', - placeholder_text: 'enter password', - onAccept: (selfInp) => { - connecting.value = staging.value.bssid || ''; - Utils.execAsync( - `nmcli dev wifi connect ${staging.value.bssid} password ${selfInp.text}`, - ) - .catch((err) => { - connecting.value = ''; - console.error(`Failed to connect to wifi: ${staging.value.ssid}... ${err}`); - Utils.notify({ - summary: 'Network', - body: err, - timeout: 5000, - }); - }) - .then(() => { - connecting.value = ''; - staging.value = {} as AccessPoint; - }); - selfInp.text = ''; - }, - }), - Widget.Button({ - hpack: 'end', - class_name: 'close-network-password-input-button', - on_primary_click: () => { - connecting.value = ''; - staging.value = {} as AccessPoint; - }, - child: Widget.Icon({ - class_name: 'close-network-password-input-icon', - icon: 'window-close-symbolic', - }), - }), - ], - }), - ], - })); - }); -}; - -export { renderWapStaging }; diff --git a/modules/menus/network/wifi/WirelessAPs.ts b/modules/menus/network/wifi/WirelessAPs.ts deleted file mode 100644 index 20a3f89ff..000000000 --- a/modules/menus/network/wifi/WirelessAPs.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { Network } from 'types/service/network.js'; -import { AccessPoint, WifiStatus } from 'lib/types/network.js'; -import { Variable } from 'types/variable.js'; -import { getWifiIcon } from '../utils.js'; -import { WIFI_STATUS_MAP } from 'globals/network.js'; -import { Attribute, Child } from 'lib/types/widget.js'; -import Box from 'types/widgets/box.js'; -const renderWAPs = ( - self: Box, - network: Network, - staging: Variable, - connecting: Variable, -): void => { - const getIdBySsid = (ssid: string, nmcliOutput: string): string | undefined => { - const lines = nmcliOutput.trim().split('\n'); - for (const line of lines) { - const columns = line.trim().split(/\s{2,}/); - if (columns[0].includes(ssid)) { - return columns[1]; - } - } - }; - - const isValidWifiStatus = (status: string): status is WifiStatus => { - return status in WIFI_STATUS_MAP; - }; - - const getWifiStatus = (): string => { - const wifiState = network.wifi.state?.toLowerCase(); - - if (wifiState && isValidWifiStatus(wifiState)) { - return WIFI_STATUS_MAP[wifiState]; - } - return WIFI_STATUS_MAP['unknown']; - }; - - self.hook(network, () => { - Utils.merge([staging.bind('value'), connecting.bind('value')], () => { - // NOTE: Sometimes the network service will yield a "this._device is undefined" when - // trying to access the "access_points" property. So we must validate that - // it's not 'undefined' - // -- - // Also this is an AGS bug that needs to be fixed - - // TODO: Remove @ts-ignore once AGS bug is fixed - // @ts-expect-error to fix AGS bug - let WAPs = network.wifi._device !== undefined ? network.wifi['access_points'] : []; - - const dedupeWAPs = (): AccessPoint[] => { - const dedupMap: Record = {}; - WAPs.forEach((item: AccessPoint) => { - if (item.ssid !== null && !Object.hasOwnProperty.call(dedupMap, item.ssid)) { - dedupMap[item.ssid] = item; - } - }); - - return Object.keys(dedupMap).map((itm) => dedupMap[itm]); - }; - - WAPs = dedupeWAPs(); - - const isInStaging = (wap: AccessPoint): boolean => { - if (Object.keys(staging.value).length === 0) { - return false; - } - - return wap.bssid === staging.value.bssid; - }; - - const isDisconnecting = (wap: AccessPoint): boolean => { - if (wap.ssid === network.wifi.ssid) { - return network.wifi.state.toLowerCase() === 'deactivating'; - } - return false; - }; - - const filteredWAPs = WAPs.filter((ap: AccessPoint) => { - return ap.ssid !== 'Unknown' && !isInStaging(ap); - }).sort((a: AccessPoint, b: AccessPoint) => { - if (network.wifi.ssid === a.ssid) { - return -1; - } - - if (network.wifi.ssid === b.ssid) { - return 1; - } - - return b.strength - a.strength; - }); - - if (filteredWAPs.length <= 0 && Object.keys(staging.value).length === 0) { - return (self.child = Widget.Label({ - class_name: 'waps-not-found dim', - expand: true, - hpack: 'center', - vpack: 'center', - label: 'No Wi-Fi Networks Found', - })); - } - return (self.children = filteredWAPs.map((ap: AccessPoint) => { - return Widget.Box({ - children: [ - Widget.Button({ - on_primary_click: () => { - if (ap.bssid === connecting.value || ap.active) { - return; - } - - connecting.value = ap.bssid || ''; - Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`) - .then(() => { - connecting.value = ''; - staging.value = {} as AccessPoint; - }) - .catch((err) => { - if (err.toLowerCase().includes('secrets were required, but not provided')) { - staging.value = ap; - } else { - Utils.notify({ - summary: 'Network', - body: err, - timeout: 5000, - }); - } - connecting.value = ''; - }); - }, - class_name: 'network-element-item', - child: Widget.Box({ - hexpand: true, - children: [ - Widget.Box({ - hpack: 'start', - hexpand: true, - children: [ - Widget.Label({ - vpack: 'start', - class_name: `network-icon wifi ${ap.ssid === network.wifi.ssid ? 'active' : ''} txt-icon`, - label: getWifiIcon(`${ap['iconName']}`), - }), - Widget.Box({ - class_name: 'connection-container', - vpack: 'center', - vertical: true, - children: [ - Widget.Label({ - vpack: 'center', - class_name: 'active-connection', - hpack: 'start', - truncate: 'end', - wrap: true, - label: ap.ssid, - }), - Widget.Revealer({ - revealChild: ap.ssid === network.wifi.ssid, - child: Widget.Label({ - hpack: 'start', - class_name: 'connection-status dim', - label: getWifiStatus(), - }), - }), - ], - }), - ], - }), - Widget.Revealer({ - hpack: 'end', - vpack: 'start', - reveal_child: ap.bssid === connecting.value || isDisconnecting(ap), - child: Widget.Spinner({ - vpack: 'start', - class_name: 'spinner wap', - }), - }), - ], - }), - }), - Widget.Revealer({ - vpack: 'start', - reveal_child: ap.bssid !== connecting.value && ap.active, - child: Widget.Box({ - children: [ - Widget.Button({ - class_name: 'menu-icon-button network disconnect', - child: Widget.Label({ - tooltip_text: 'Disconnect', - class_name: 'menu-icon-button disconnect-network txt-icon', - label: '󱘖', - }), - on_primary_click: () => { - connecting.value = ap.bssid || ''; - Utils.execAsync('nmcli connection show --active').then(() => { - Utils.execAsync('nmcli connection show --active').then((res) => { - const connectionId = getIdBySsid(ap.ssid || '', res); - - if (connectionId === undefined) { - console.error( - `Error while disconnecting "${ap.ssid}": Connection ID not found`, - ); - return; - } - - Utils.execAsync( - `nmcli connection down ${connectionId} "${ap.ssid}"`, - ) - .then(() => (connecting.value = '')) - .catch((err) => { - connecting.value = ''; - console.error( - `Error while disconnecting "${ap.ssid}": ${err}`, - ); - }); - }); - }); - }, - }), - Widget.Button({ - tooltip_text: 'Delete/Forget Network', - class_name: 'menu-icon-button network disconnect', - on_primary_click: () => { - connecting.value = ap.bssid || ''; - Utils.execAsync('nmcli connection show --active').then(() => { - Utils.execAsync('nmcli connection show --active').then((res) => { - const connectionId = getIdBySsid(ap.ssid || '', res); - - if (connectionId === undefined) { - console.error( - `Error while forgetting "${ap.ssid}": Connection ID not found`, - ); - return; - } - - Utils.execAsync( - `nmcli connection delete ${connectionId} "${ap.ssid}"`, - ) - .then(() => (connecting.value = '')) - .catch((err) => { - connecting.value = ''; - console.error( - `Error while forgetting "${ap.ssid}": ${err}`, - ); - }); - }); - }); - }, - child: Widget.Label({ - class_name: 'txt-icon delete-network', - label: '󰚃', - }), - }), - ], - }), - }), - ], - }); - })); - }); - }); -}; - -export { renderWAPs }; diff --git a/modules/menus/network/wifi/index.ts b/modules/menus/network/wifi/index.ts deleted file mode 100644 index 57948f663..000000000 --- a/modules/menus/network/wifi/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -const network = await Service.import('network'); -import { renderWAPs } from './WirelessAPs.js'; -import { renderWapStaging } from './APStaging.js'; -import { AccessPoint } from 'lib/types/network.js'; -import { BoxWidget } from 'lib/types/widget.js'; - -const Staging = Variable({} as AccessPoint); -const Connecting = Variable(''); - -const searchInProgress = Variable(false); - -const startRotation = (): void => { - searchInProgress.value = true; - setTimeout(() => { - searchInProgress.value = false; - }, 5 * 1000); -}; - -const Wifi = (): BoxWidget => { - return Widget.Box({ - class_name: 'menu-section-container wifi', - vertical: true, - children: [ - Widget.Box({ - class_name: 'menu-label-container', - hpack: 'fill', - children: [ - Widget.Label({ - class_name: 'menu-label', - hexpand: true, - hpack: 'start', - label: 'Wi-Fi', - }), - Widget.Switch({ - class_name: 'menu-switch network', - vpack: 'center', - tooltip_text: 'Toggle Wifi', - active: network.wifi.enabled, - on_activate: () => { - network.toggleWifi(); - }, - }), - Widget.Button({ - vpack: 'center', - hpack: 'end', - class_name: 'menu-icon-button search network', - on_primary_click: () => { - startRotation(); - network.wifi.scan(); - }, - child: Widget.Icon({ - class_name: searchInProgress.bind('value').as((v) => (v ? 'spinning' : '')), - icon: 'view-refresh-symbolic', - }), - }), - ], - }), - Widget.Box({ - class_name: 'menu-items-section', - vertical: true, - children: [ - Widget.Box({ - class_name: 'wap-staging', - setup: (self) => { - renderWapStaging(self, network, Staging, Connecting); - }, - }), - Widget.Box({ - class_name: 'available-waps', - vertical: true, - setup: (self) => { - renderWAPs(self, network, Staging, Connecting); - }, - }), - ], - }), - ], - }); -}; - -export { Wifi }; diff --git a/modules/menus/notifications/controls/index.ts b/modules/menus/notifications/controls/index.ts deleted file mode 100644 index de4492d51..000000000 --- a/modules/menus/notifications/controls/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { clearNotifications } from 'globals/notification'; -import { BoxWidget } from 'lib/types/widget'; -import { Notifications } from 'types/service/notifications'; -import options from 'options'; - -const { clearDelay } = options.notifications; - -const Controls = (notifs: Notifications): BoxWidget => { - return Widget.Box({ - class_name: 'notification-menu-controls', - expand: false, - vertical: false, - children: [ - Widget.Box({ - class_name: 'menu-label-container notifications', - hpack: 'start', - vpack: 'center', - expand: true, - children: [ - Widget.Label({ - class_name: 'menu-label notifications', - label: 'Notifications', - }), - ], - }), - Widget.Box({ - hpack: 'end', - vpack: 'center', - expand: false, - children: [ - Widget.Switch({ - class_name: 'menu-switch notifications', - vpack: 'center', - active: notifs.bind('dnd').as((dnd: boolean) => !dnd), - on_activate: ({ active }) => { - notifs.dnd = !active; - }, - }), - Widget.Box({ - children: [ - Widget.Separator({ - hpack: 'center', - vexpand: true, - vertical: true, - class_name: 'menu-separator notification-controls', - }), - Widget.Button({ - className: 'clear-notifications-button', - tooltip_text: 'Clear Notifications', - on_primary_click: clearDelay.bind('value').as((delay) => { - return () => { - if (removingNotifications.value) { - return; - } - - return clearNotifications(notifs.notifications, delay); - }; - }), - child: Widget.Label({ - class_name: removingNotifications.bind('value').as((removing: boolean) => { - return removing - ? 'clear-notifications-label txt-icon removing' - : 'clear-notifications-label txt-icon'; - }), - label: '', - }), - }), - ], - }), - ], - }), - ], - }); -}; - -export { Controls }; diff --git a/modules/menus/notifications/index.ts b/modules/menus/notifications/index.ts deleted file mode 100644 index bbdbce28d..000000000 --- a/modules/menus/notifications/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Notification } from 'types/service/notifications.js'; -import DropdownMenu from '../shared/dropdown/index.js'; -const notifs = await Service.import('notifications'); -import { Controls } from './controls/index.js'; -import { NotificationCard } from './notification/index.js'; -import { NotificationPager } from './pager/index.js'; - -import options from 'options'; -import Window from 'types/widgets/window.js'; -import { Attribute, Child } from 'lib/types/widget.js'; - -const { displayedTotal } = options.notifications; - -export default (): Window => { - const curPage = Variable(1); - - Utils.merge( - [curPage.bind('value'), displayedTotal.bind('value'), notifs.bind('notifications')], - (currentPage: number, dispTotal: number, notifications: Notification[]) => { - // If the page doesn't have enough notifications to display, go back - // to the previous page. - if (notifications.length <= (currentPage - 1) * dispTotal) { - curPage.value = currentPage <= 1 ? 1 : currentPage - 1; - } - }, - ); - - return DropdownMenu({ - name: 'notificationsmenu', - transition: options.menus.transition.bind('value'), - child: Widget.Box({ - class_name: 'notification-menu-content', - css: 'padding: 1px; margin: -1px;', - hexpand: true, - vexpand: false, - children: [ - Widget.Box({ - class_name: 'notification-card-container menu', - vertical: true, - hexpand: false, - vexpand: false, - children: [Controls(notifs), NotificationCard(notifs, curPage), NotificationPager(curPage)], - }), - ], - }), - }); -}; diff --git a/modules/menus/notifications/notification/actions/index.ts b/modules/menus/notifications/notification/actions/index.ts deleted file mode 100644 index bf68dad43..000000000 --- a/modules/menus/notifications/notification/actions/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BoxWidget } from 'lib/types/widget'; -import { Notification, Notifications } from 'types/service/notifications'; -const Actions = (notif: Notification, notifs: Notifications): BoxWidget => { - if (notif.actions !== undefined && notif.actions.length > 0) { - return Widget.Box({ - class_name: 'notification-card-actions menu', - hexpand: true, - vpack: 'end', - children: notif.actions.map((action) => { - return Widget.Button({ - hexpand: true, - class_name: 'notification-action-buttons menu', - on_primary_click: () => { - if (action.id.includes('scriptAction:-')) { - App.closeWindow('notificationsmenu'); - Utils.execAsync(`${action.id.replace('scriptAction:-', '')}`).catch((err) => - console.error(err), - ); - notifs.CloseNotification(notif.id); - } else { - App.closeWindow('notificationsmenu'); - notif.invoke(action.id); - } - }, - child: Widget.Box({ - hpack: 'center', - hexpand: true, - children: [ - Widget.Label({ - class_name: 'notification-action-buttons-label menu', - hexpand: true, - max_width_chars: 15, - truncate: 'end', - wrap: true, - label: action.label, - }), - ], - }), - }); - }), - }); - } - - return Widget.Box({ - class_name: 'spacer', - }); -}; - -export { Actions }; diff --git a/modules/menus/notifications/notification/body/index.ts b/modules/menus/notifications/notification/body/index.ts deleted file mode 100644 index dd566af6c..000000000 --- a/modules/menus/notifications/notification/body/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { BoxWidget } from 'lib/types/widget.js'; -import { notifHasImg } from '../../utils.js'; -import { Notification } from 'types/service/notifications'; - -export const Body = (notif: Notification): BoxWidget => { - return Widget.Box({ - vpack: 'start', - hexpand: true, - class_name: 'notification-card-body menu', - children: [ - Widget.Label({ - hexpand: true, - use_markup: true, - xalign: 0, - justification: 'left', - truncate: 'end', - lines: 2, - max_width_chars: !notifHasImg(notif) ? 35 : 28, - wrap: true, - class_name: 'notification-card-body-label menu', - label: notif['body'], - }).on('realize', (self) => { - self.set_markup(notif['body']); - }), - ], - }); -}; diff --git a/modules/menus/notifications/notification/close/index.ts b/modules/menus/notifications/notification/close/index.ts deleted file mode 100644 index 261f9490e..000000000 --- a/modules/menus/notifications/notification/close/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Attribute } from 'lib/types/widget'; -import { Notification, Notifications } from 'types/service/notifications'; -import Button from 'types/widgets/button'; -import Label from 'types/widgets/label'; -export const CloseButton = (notif: Notification, notifs: Notifications): Button, Attribute> => { - return Widget.Button({ - class_name: 'close-notification-button menu', - on_primary_click: () => { - notifs.CloseNotification(notif.id); - }, - child: Widget.Label({ - class_name: 'txt-icon notif-close', - label: '󰅜', - hpack: 'center', - }), - }); -}; diff --git a/modules/menus/notifications/notification/header/icon.ts b/modules/menus/notifications/notification/header/icon.ts deleted file mode 100644 index c51bc0108..000000000 --- a/modules/menus/notifications/notification/header/icon.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Notification } from 'types/service/notifications.js'; -import { NotificationIcon } from 'lib/types/notification.js'; -import { getNotificationIcon } from 'globals/notification'; -import { BoxWidget } from 'lib/types/widget'; - -const NotificationIcon = ({ app_entry = '', app_icon = '', app_name = '' }: Partial): BoxWidget => { - return Widget.Box({ - css: ` - min-width: 2rem; - min-height: 2rem; - `, - child: Widget.Icon({ - class_name: 'notification-icon menu', - icon: getNotificationIcon(app_name, app_icon, app_entry), - }), - }); -}; - -export { NotificationIcon }; diff --git a/modules/menus/notifications/notification/header/index.ts b/modules/menus/notifications/notification/header/index.ts deleted file mode 100644 index 70fe58c0f..000000000 --- a/modules/menus/notifications/notification/header/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import GLib from 'gi://GLib'; -import { Notification } from 'types/service/notifications'; -import { NotificationIcon } from './icon.js'; -import { notifHasImg } from '../../utils.js'; -import options from 'options.js'; -import { BoxWidget } from 'lib/types/widget.js'; - -const { military } = options.menus.clock.time; - -export const Header = (notif: Notification): BoxWidget => { - const time = (time: number, format = '%I:%M %p'): string => { - return GLib.DateTime.new_from_unix_local(time).format(military.value ? '%H:%M' : format) || '--:--'; - }; - - return Widget.Box({ - vertical: false, - hexpand: true, - children: [ - Widget.Box({ - class_name: 'notification-card-header menu', - hpack: 'start', - children: [NotificationIcon(notif)], - }), - Widget.Box({ - class_name: 'notification-card-header menu', - hexpand: true, - vpack: 'start', - children: [ - Widget.Label({ - class_name: 'notification-card-header-label menu', - hpack: 'start', - hexpand: true, - vexpand: true, - max_width_chars: !notifHasImg(notif) ? 34 : 22, - truncate: 'end', - wrap: true, - label: notif['summary'], - }).on('realize', (self) => { - self.set_markup(notif['summary']); - }), - ], - }), - Widget.Box({ - class_name: 'notification-card-header menu', - hpack: 'end', - vpack: 'start', - hexpand: true, - child: Widget.Label({ - vexpand: true, - class_name: 'notification-time', - label: time(notif.time), - }), - }), - ], - }); -}; diff --git a/modules/menus/notifications/notification/image/index.ts b/modules/menus/notifications/notification/image/index.ts deleted file mode 100644 index 6b01768c8..000000000 --- a/modules/menus/notifications/notification/image/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Notification } from 'types/service/notifications'; -import { notifHasImg } from '../../utils.js'; -import { BoxWidget } from 'lib/types/widget.js'; - -const Image = (notif: Notification): BoxWidget => { - if (notifHasImg(notif)) { - return Widget.Box({ - class_name: 'notification-card-image-container menu', - hpack: 'center', - vpack: 'center', - vexpand: false, - child: Widget.Box({ - hpack: 'center', - vexpand: false, - class_name: 'notification-card-image menu', - css: `background-image: url("${notif.image}")`, - }), - }); - } - - return Widget.Box(); -}; - -export { Image }; diff --git a/modules/menus/notifications/notification/index.ts b/modules/menus/notifications/notification/index.ts deleted file mode 100644 index 776c40903..000000000 --- a/modules/menus/notifications/notification/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Notifications, Notification } from 'types/service/notifications'; -import { notifHasImg } from '../utils.js'; -import { Header } from './header/index.js'; -import { Actions } from './actions/index.js'; -import { Image } from './image/index.js'; -import { Placeholder } from './placeholder/index.js'; -import { Body } from './body/index.js'; -import { CloseButton } from './close/index.js'; -import options from 'options.js'; -import { Variable } from 'types/variable.js'; -import { filterNotifications } from 'lib/shared/notifications.js'; -import Scrollable from 'types/widgets/scrollable.js'; -import { Attribute, Child } from 'lib/types/widget.js'; - -const { displayedTotal, ignore, showActionsOnHover } = options.notifications; - -const NotificationCard = (notifs: Notifications, curPage: Variable): Scrollable => { - return Widget.Scrollable({ - vscroll: 'automatic', - child: Widget.Box({ - class_name: 'menu-content-container notifications', - vexpand: true, - hpack: 'fill', - spacing: 0, - vertical: true, - setup: (self) => { - Utils.merge( - [ - notifs.bind('notifications'), - curPage.bind('value'), - displayedTotal.bind('value'), - ignore.bind('value'), - showActionsOnHover.bind('value'), - ], - (notifications, currentPage, dispTotal, ignoredNotifs, showActions) => { - const filteredNotifications = filterNotifications(notifications, ignoredNotifs); - - const sortedNotifications = filteredNotifications.sort((a, b) => b.time - a.time); - - if (filteredNotifications.length <= 0) { - return (self.children = [Placeholder(notifs)]); - } - - const pageStart = (currentPage - 1) * dispTotal; - const pageEnd = currentPage * dispTotal; - return (self.children = sortedNotifications - .slice(pageStart, pageEnd) - .map((notif: Notification) => { - const actionsbox = - notif.actions.length > 0 - ? Widget.Revealer({ - transition: 'slide_down', - reveal_child: showActions ? false : true, - child: Widget.EventBox({ - child: Actions(notif, notifs), - }), - }) - : null; - - return Widget.EventBox({ - on_hover() { - if (actionsbox && showActions) actionsbox.reveal_child = true; - }, - on_hover_lost() { - if (actionsbox && showActions) actionsbox.reveal_child = false; - }, - child: Widget.Box({ - class_name: 'notification-card-content-container', - hexpand: true, - children: [ - Widget.Box({ - class_name: 'notification-card menu', - vpack: 'start', - vexpand: false, - children: [ - Image(notif), - Widget.Box({ - vpack: 'center', - vertical: true, - hexpand: true, - class_name: `notification-card-content ${!notifHasImg(notif) ? 'noimg' : ' menu'}`, - - children: actionsbox - ? [Header(notif), Body(notif), actionsbox] - : [Header(notif), Body(notif)], - }), - ], - }), - CloseButton(notif, notifs), - ], - }), - }); - })); - }, - ); - }, - }), - }); -}; - -export { NotificationCard }; diff --git a/modules/menus/notifications/notification/placeholder/index.ts b/modules/menus/notifications/notification/placeholder/index.ts deleted file mode 100644 index e22df5533..000000000 --- a/modules/menus/notifications/notification/placeholder/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { BoxWidget } from 'lib/types/widget'; -import { Notifications } from 'types/service/notifications'; - -const Placeholder = (notifs: Notifications): BoxWidget => { - return Widget.Box({ - class_name: 'notification-label-container', - vpack: 'fill', - hpack: 'center', - expand: true, - child: Widget.Box({ - vpack: 'center', - vertical: true, - expand: true, - children: [ - Widget.Label({ - vpack: 'center', - class_name: 'placeholder-label dim bell txt-icon', - label: notifs.bind('dnd').as((dnd) => (dnd ? '󰂛' : '󰂚')), - }), - Widget.Label({ - vpack: 'start', - class_name: 'placehold-label dim message', - label: "You're all caught up :)", - }), - ], - }), - }); -}; - -export { Placeholder }; diff --git a/modules/menus/notifications/pager/index.ts b/modules/menus/notifications/pager/index.ts deleted file mode 100644 index 1a4571bf4..000000000 --- a/modules/menus/notifications/pager/index.ts +++ /dev/null @@ -1,88 +0,0 @@ -const notifs = await Service.import('notifications'); - -import { BoxWidget } from 'lib/types/widget'; -import options from 'options'; -import { Notification } from 'types/service/notifications'; -import { Variable } from 'types/variable'; - -const { displayedTotal } = options.notifications; -const { show: showPager } = options.theme.bar.menus.menu.notifications.pager; - -export const NotificationPager = (curPage: Variable): BoxWidget => { - return Widget.Box({ - class_name: 'notification-menu-pager', - hexpand: true, - vexpand: false, - children: Utils.merge( - [ - curPage.bind('value'), - displayedTotal.bind('value'), - notifs.bind('notifications'), - showPager.bind('value'), - ], - (currentPage: number, dispTotal: number, _: Notification[], showPgr: boolean) => { - if (showPgr === false || (currentPage === 1 && notifs.notifications.length <= dispTotal)) { - return []; - } - return [ - Widget.Button({ - hexpand: true, - hpack: 'start', - class_name: `pager-button left ${currentPage <= 1 ? 'disabled' : ''}`, - onPrimaryClick: () => { - curPage.value = 1; - }, - child: Widget.Label({ - className: 'pager-button-label', - label: '', - }), - }), - Widget.Button({ - hexpand: true, - hpack: 'start', - class_name: `pager-button left ${currentPage <= 1 ? 'disabled' : ''}`, - onPrimaryClick: () => { - curPage.value = currentPage <= 1 ? 1 : currentPage - 1; - }, - child: Widget.Label({ - className: 'pager-button-label', - label: '', - }), - }), - Widget.Label({ - hexpand: true, - hpack: 'center', - class_name: 'pager-label', - label: `${currentPage} / ${Math.ceil(notifs.notifications.length / dispTotal) || 1}`, - }), - Widget.Button({ - hexpand: true, - hpack: 'end', - class_name: `pager-button right ${currentPage >= Math.ceil(notifs.notifications.length / dispTotal) ? 'disabled' : ''}`, - onPrimaryClick: () => { - const maxPage = Math.ceil(notifs.notifications.length / displayedTotal.value); - curPage.value = currentPage >= maxPage ? currentPage : currentPage + 1; - }, - child: Widget.Label({ - className: 'pager-button-label', - label: '', - }), - }), - Widget.Button({ - hexpand: true, - hpack: 'end', - class_name: `pager-button right ${currentPage >= Math.ceil(notifs.notifications.length / dispTotal) ? 'disabled' : ''}`, - onPrimaryClick: () => { - const maxPage = Math.ceil(notifs.notifications.length / displayedTotal.value); - curPage.value = maxPage; - }, - child: Widget.Label({ - className: 'pager-button-label', - label: '󰄾', - }), - }), - ]; - }, - ), - }); -}; diff --git a/modules/menus/notifications/utils.ts b/modules/menus/notifications/utils.ts deleted file mode 100644 index dcf2c7de6..000000000 --- a/modules/menus/notifications/utils.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Notification } from 'types/service/notifications'; - -const notifHasImg = (notif: Notification): boolean => { - return notif.image !== undefined && notif.image.length ? true : false; -}; - -export { notifHasImg }; diff --git a/modules/menus/power/helpers/actions.ts b/modules/menus/power/helpers/actions.ts deleted file mode 100644 index fee7dfa0c..000000000 --- a/modules/menus/power/helpers/actions.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Action } from 'lib/types/power'; -import options from 'options'; -const { sleep, reboot, logout, shutdown } = options.menus.dashboard.powermenu; - -class PowerMenu extends Service { - static { - Service.register( - this, - {}, - { - title: ['string'], - cmd: ['string'], - }, - ); - } - - #title = ''; - #cmd = ''; - - get title(): string { - return this.#title; - } - - action(action: Action): void { - [this.#cmd, this.#title] = { - sleep: [sleep.value, 'Sleep'], - reboot: [reboot.value, 'Reboot'], - logout: [logout.value, 'Log Out'], - shutdown: [shutdown.value, 'Shutdown'], - }[action]; - - this.notify('cmd'); - this.notify('title'); - this.emit('changed'); - App.closeWindow('powermenu'); - App.openWindow('verification'); - } - - customAction(action: Action, cmnd: string): void { - [this.#cmd, this.#title] = [cmnd, action]; - - this.notify('cmd'); - this.notify('title'); - this.emit('changed'); - App.closeWindow('powermenu'); - App.openWindow('verification'); - } - - shutdown = (): void => { - this.action('shutdown'); - }; - - exec = (): void => { - App.closeWindow('verification'); - Utils.execAsync(this.#cmd); - }; -} - -const powermenu = new PowerMenu(); -Object.assign(globalThis, { powermenu }); -export default powermenu; diff --git a/modules/menus/power/index.ts b/modules/menus/power/index.ts deleted file mode 100644 index bb8505dbe..000000000 --- a/modules/menus/power/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Action } from 'lib/types/power.js'; -import PopupWindow from '../shared/popup/index.js'; -import powermenu from './helpers/actions.js'; -import icons from '../../icons/index.js'; -import Window from 'types/widgets/window.js'; -import { Attribute, Child, GButton } from 'lib/types/widget.js'; -import options from 'options.js'; - -const SysButton = (action: Action, label: string): GButton => - Widget.Button({ - class_name: `widget-button powermenu-button-${action}`, - on_clicked: () => powermenu.action(action), - child: Widget.Box({ - vertical: true, - class_name: 'system-button widget-box', - children: [ - Widget.Icon({ - class_name: `system-button_icon ${action}`, - icon: icons.powermenu[action], - }), - Widget.Label({ - class_name: `system-button_label ${action}`, - label, - }), - ], - }), - }); -export default (): Window => - PopupWindow({ - name: 'powermenu', - transition: options.menus.transition.bind('value'), - child: Widget.Box({ - class_name: 'powermenu horizontal', - children: [ - SysButton('shutdown', 'SHUTDOWN'), - SysButton('logout', 'LOG OUT'), - SysButton('reboot', 'REBOOT'), - SysButton('sleep', 'SLEEP'), - ], - }), - }); diff --git a/modules/menus/power/verification.ts b/modules/menus/power/verification.ts deleted file mode 100644 index a71f46bf7..000000000 --- a/modules/menus/power/verification.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Window from 'types/widgets/window.js'; -import PopupWindow from '../shared/popup/index.js'; -import powermenu from './helpers/actions.js'; -import { Attribute, Child } from 'lib/types/widget.js'; - -export default (): Window => - PopupWindow({ - name: 'verification', - transition: 'crossfade', - child: Widget.Box({ - class_name: 'verification', - child: Widget.Box({ - class_name: 'verification-content', - expand: true, - vertical: true, - children: [ - Widget.Box({ - class_name: 'text-box', - vertical: true, - children: [ - Widget.Label({ - class_name: 'title', - label: powermenu.bind('title').as((t) => t.toUpperCase()), - }), - Widget.Label({ - class_name: 'desc', - label: powermenu - .bind('title') - .as((p) => `Are you sure you want to ${p.toLowerCase()}?`), - }), - ], - }), - Widget.Box({ - class_name: 'buttons horizontal', - vexpand: true, - vpack: 'end', - homogeneous: true, - children: [ - Widget.Button({ - class_name: 'verification-button bar-verification_yes', - child: Widget.Label('Yes'), - on_clicked: powermenu.exec, - }), - Widget.Button({ - class_name: 'verification-button bar-verification_no', - child: Widget.Label('No'), - on_clicked: () => App.toggleWindow('verification'), - }), - ], - }), - ], - }), - }), - }); diff --git a/modules/menus/powerDropdown/button.ts b/modules/menus/powerDropdown/button.ts deleted file mode 100644 index 4b0d017c7..000000000 --- a/modules/menus/powerDropdown/button.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { PowerOptions } from 'lib/types/options'; -import { GButton } from 'lib/types/widget'; -import { capitalizeFirstLetter } from 'lib/utils'; -import options from 'options'; -import powermenu from '../power/helpers/actions'; - -const { confirmation, shutdown, logout, sleep, reboot, showLabel } = options.menus.power; - -export const PowerButton = (action: PowerOptions): GButton => { - const handleClick = (action: PowerOptions): void => { - const actions = { - shutdown: shutdown.value, - reboot: reboot.value, - logout: logout.value, - sleep: sleep.value, - }; - App.closeWindow('powerdropdownmenu'); - - if (!confirmation.value) { - Utils.execAsync(actions[action]).catch((err) => - console.error(`Failed to execute ${action} command. Error: ${err}`), - ); - } else { - powermenu.customAction(action, actions[action]); - } - }; - - const powerIconMap = { - shutdown: '󰐥', - reboot: '󰜉', - logout: '󰿅', - sleep: '󰤄', - }; - - return Widget.Button({ - className: showLabel.bind('value').as((shwLbl) => { - return `power-menu-button ${action} ${!shwLbl ? 'no-label' : ''}`; - }), - on_clicked: () => handleClick(action), - child: Widget.Box({ - vertical: false, - children: showLabel.bind('value').as((shwLbl) => { - if (shwLbl) { - return [ - Widget.Label({ - label: powerIconMap[action], - className: `power-button-icon ${action}-icon txt-icon`, - }), - Widget.Label({ - hpack: 'center', - hexpand: true, - label: capitalizeFirstLetter(action), - className: `power-button-label ${action}-label show-label`, - }), - ]; - } - return [ - Widget.Label({ - label: powerIconMap[action], - className: `power-button-icon ${action}-icon no-label txt-icon`, - }), - ]; - }), - }), - }); -}; diff --git a/modules/menus/powerDropdown/index.ts b/modules/menus/powerDropdown/index.ts deleted file mode 100644 index 1562d8bf8..000000000 --- a/modules/menus/powerDropdown/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import Window from 'types/widgets/window.js'; -import DropdownMenu from '../shared/dropdown/index.js'; -import { PowerButton } from './button.js'; -import { Attribute, Child } from 'lib/types/widget.js'; -import options from 'options.js'; - -export default (): Window => { - return DropdownMenu({ - name: 'powerdropdownmenu', - transition: options.menus.transition.bind('value'), - child: Widget.Box({ - class_name: 'menu-items power-dropdown', - child: Widget.Box({ - vertical: true, - hexpand: true, - class_name: 'menu-items-container power-dropdown', - children: [PowerButton('shutdown'), PowerButton('reboot'), PowerButton('logout'), PowerButton('sleep')], - }), - }), - }); -}; diff --git a/modules/menus/shared/dropdown/eventBoxes/index.ts b/modules/menus/shared/dropdown/eventBoxes/index.ts deleted file mode 100644 index 727610d50..000000000 --- a/modules/menus/shared/dropdown/eventBoxes/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Attribute, BoxWidget } from 'lib/types/widget'; -import EventBox from 'types/widgets/eventbox'; -import { BarLocation } from 'lib/types/options'; - -const createEventBox = (className: string, windowName: string): EventBox => { - return Widget.EventBox({ - class_name: className, - hexpand: true, - vexpand: false, - can_focus: false, - child: Widget.Box(), - setup: (w) => { - w.on('button-press-event', () => App.toggleWindow(windowName)); - }, - }); -}; - -export const barEventMargins = ( - windowName: string, - location: BarLocation = 'top', -): [EventBox, EventBox] => { - if (location === 'top') { - return [ - createEventBox('mid-eb event-top-padding-static', windowName), - createEventBox('mid-eb event-top-padding', windowName), - ]; - } else { - return [ - createEventBox('mid-eb event-bottom-padding', windowName), - createEventBox('mid-eb event-bottom-padding-static', windowName), - ]; - } -}; diff --git a/modules/menus/shared/dropdown/index.ts b/modules/menus/shared/dropdown/index.ts deleted file mode 100644 index 7ae605f59..000000000 --- a/modules/menus/shared/dropdown/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -import options from 'options'; -import { DropdownMenuProps } from 'lib/types/dropdownmenu'; -import { Attribute, Child, Exclusivity } from 'lib/types/widget'; -import Window from 'types/widgets/window'; -import { barEventMargins } from './eventBoxes/index'; -import { globalEventBoxes } from 'globals/dropdown'; - -const { location } = options.theme.bar; - -// NOTE: We make the window visible for 2 seconds (on startup) so the child -// elements can allocate their proper dimensions. -// Otherwise the width that we rely on for menu positioning is set improperly -// for the first time we open a menu of each type. -const initRender = Variable(true); - -setTimeout(() => { - initRender.value = false; -}, 2000); - -export default ({ - name, - child, - transition, - exclusivity = 'ignore' as Exclusivity, - ...props -}: DropdownMenuProps): Window => - Widget.Window({ - name, - class_names: [name, 'dropdown-menu'], - setup: (w) => w.keybind('Escape', () => App.closeWindow(name)), - visible: initRender.bind('value'), - keymode: 'on-demand', - exclusivity, - layer: 'top', - anchor: location.bind('value').as((ln) => [ln, 'left']), - child: Widget.EventBox({ - class_name: 'parent-event', - on_primary_click: () => App.closeWindow(name), - on_secondary_click: () => App.closeWindow(name), - child: Widget.Box({ - class_name: 'top-eb', - vertical: true, - children: [ - Widget.Box({ - className: 'event-box-container', - children: location.bind('value').as((lcn) => { - if (lcn === 'top') { - return barEventMargins(name); - } else { - return []; - } - }), - }), - Widget.EventBox({ - class_name: 'in-eb menu-event-box', - on_primary_click: () => { - return true; - }, - on_secondary_click: () => { - return true; - }, - setup: (self) => { - globalEventBoxes.value[name] = self; - }, - child: Widget.Box({ - class_name: 'dropdown-menu-container', - css: 'padding: 1px; margin: -1px;', - child: Widget.Revealer({ - revealChild: false, - setup: (self) => - self.hook(App, (_, wname, visible) => { - if (wname === name) self.reveal_child = visible; - }), - transition, - transitionDuration: options.menus.transitionTime.bind('value'), - child: Widget.Box({ - class_name: 'dropdown-menu-container', - can_focus: true, - children: [child], - }), - }), - }), - }), - Widget.Box({ - className: 'event-box-container', - children: location.bind('value').as((lcn) => { - if (lcn === 'bottom') { - return barEventMargins(name); - } else { - return []; - } - }), - }), - ], - }), - }), - ...props, - }); diff --git a/modules/menus/shared/dropdown/locationHandler/index.ts b/modules/menus/shared/dropdown/locationHandler/index.ts deleted file mode 100644 index 28ce7c75d..000000000 --- a/modules/menus/shared/dropdown/locationHandler/index.ts +++ /dev/null @@ -1,98 +0,0 @@ -const hyprland = await Service.import('hyprland'); - -import options from 'options'; -import { bash } from 'lib/utils'; -import { Widget as TWidget } from 'types/@girs/gtk-3.0/gtk-3.0.cjs'; -import { Monitor } from 'types/service/hyprland'; -import Box from 'types/widgets/box'; -import EventBox from 'types/widgets/eventbox'; -import Revealer from 'types/widgets/revealer'; -import { globalEventBoxes } from 'globals/dropdown'; - -type NestedRevealer = Revealer, unknown>; -type NestedBox = Box; -type NestedEventBox = EventBox; - -const { location } = options.theme.bar; -const { scalingPriority } = options; - -export const calculateMenuPosition = async (pos: number[], windowName: string): Promise => { - const self = globalEventBoxes.value[windowName] as NestedEventBox; - const curHyprlandMonitor = hyprland.monitors.find((m) => m.id === hyprland.active.monitor.id); - const dropdownWidth = self.child.get_allocation().width; - const dropdownHeight = self.child.get_allocation().height; - - let hyprScaling = 1; - try { - const monitorInfo = await bash('hyprctl monitors -j'); - const parsedMonitorInfo = JSON.parse(monitorInfo); - - const foundMonitor = parsedMonitorInfo.find((monitor: Monitor) => monitor.id === hyprland.active.monitor.id); - hyprScaling = foundMonitor?.scale || 1; - } catch (error) { - console.error(`Error parsing hyprland monitors: ${error}`); - } - - let monWidth = curHyprlandMonitor?.width; - let monHeight = curHyprlandMonitor?.height; - - if (monWidth === undefined || monHeight === undefined || hyprScaling === undefined) { - return; - } - - // If GDK Scaling is applied, then get divide width by scaling - // to get the proper coordinates. - // Ex: On a 2860px wide monitor... if scaling is set to 2, then the right - // end of the monitor is the 1430th pixel. - const gdkScale = Utils.exec('bash -c "echo $GDK_SCALE"'); - - if (scalingPriority.value === 'both') { - const scale = parseFloat(gdkScale); - monWidth = monWidth / scale; - monHeight = monHeight / scale; - - monWidth = monWidth / hyprScaling; - monHeight = monHeight / hyprScaling; - } else if (/^\d+(.\d+)?$/.test(gdkScale) && scalingPriority.value === 'gdk') { - const scale = parseFloat(gdkScale); - monWidth = monWidth / scale; - monHeight = monHeight / scale; - } else { - monWidth = monWidth / hyprScaling; - monHeight = monHeight / hyprScaling; - } - - // If monitor is vertical (transform = 1 || 3) swap height and width - const isVertical = curHyprlandMonitor?.transform !== undefined ? curHyprlandMonitor.transform % 2 !== 0 : false; - - if (isVertical) { - [monWidth, monHeight] = [monHeight, monWidth]; - } - - let marginRight = monWidth - dropdownWidth / 2; - marginRight = marginRight - pos[0]; - let marginLeft = monWidth - dropdownWidth - marginRight; - - const minimumMargin = 0; - - if (marginRight < minimumMargin) { - marginRight = minimumMargin; - marginLeft = monWidth - dropdownWidth - minimumMargin; - } - - if (marginLeft < minimumMargin) { - marginLeft = minimumMargin; - marginRight = monWidth - dropdownWidth - minimumMargin; - } - - self.set_margin_left(marginLeft); - self.set_margin_right(marginRight); - - if (location.value === 'top') { - self.set_margin_top(0); - self.set_margin_bottom(monHeight); - } else { - self.set_margin_bottom(0); - self.set_margin_top(monHeight - dropdownHeight); - } -}; diff --git a/modules/menus/shared/popup/index.ts b/modules/menus/shared/popup/index.ts deleted file mode 100644 index 3738030ac..000000000 --- a/modules/menus/shared/popup/index.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { WINDOW_LAYOUTS } from 'globals/window'; -import { LayoutFunction, Layouts, PopupWindowProps } from 'lib/types/popupwindow'; -import { Attribute, Child, Exclusivity, GtkWidget, Transition } from 'lib/types/widget'; -import Box from 'types/widgets/box'; -import EventBox from 'types/widgets/eventbox'; -import Window from 'types/widgets/window'; - -type Opts = { - className: string; - vexpand: boolean; -}; - -export const Padding = (name: string, opts: Opts): EventBox, unknown> => - Widget.EventBox({ - class_name: opts?.className || '', - hexpand: true, - vexpand: typeof opts?.vexpand === 'boolean' ? opts.vexpand : true, - can_focus: false, - child: Widget.Box(), - setup: (w) => w.on('button-press-event', () => App.toggleWindow(name)), - }); - -const PopupRevealer = ( - name: string, - child: GtkWidget, - transition = 'slide_down' as Transition, -): Box => - Widget.Box( - { css: 'padding: 1px;' }, - Widget.Revealer({ - transition, - child: Widget.Box({ - class_name: `window-content ${name}-window`, - child, - }), - transitionDuration: 200, - setup: (self) => - self.hook(App, (_, wname, visible) => { - if (wname === name) self.reveal_child = visible; - }), - }), - ); - -const Layout: LayoutFunction = (name: string, child: GtkWidget, transition: Transition) => ({ - center: () => - Widget.CenterBox( - {}, - Padding(name, {} as Opts), - Widget.CenterBox( - { vertical: true }, - Padding(name, {} as Opts), - PopupRevealer(name, child, transition), - Padding(name, {} as Opts), - ), - Padding(name, {} as Opts), - ), - top: () => - Widget.CenterBox( - {}, - Padding(name, {} as Opts), - Widget.Box({ vertical: true }, PopupRevealer(name, child, transition), Padding(name, {} as Opts)), - Padding(name, {} as Opts), - ), - 'top-right': () => - Widget.Box( - {}, - Padding(name, {} as Opts), - Widget.Box( - { - hexpand: false, - vertical: true, - }, - Padding(name, { - vexpand: false, - className: 'event-top-padding', - }), - PopupRevealer(name, child, transition), - Padding(name, {} as Opts), - ), - ), - 'top-center': () => - Widget.Box( - {}, - Padding(name, {} as Opts), - Widget.Box( - { - hexpand: false, - vertical: true, - }, - Padding(name, { - vexpand: false, - className: 'event-top-padding', - }), - PopupRevealer(name, child, transition), - Padding(name, {} as Opts), - ), - Padding(name, {} as Opts), - ), - 'top-left': () => - Widget.Box( - {}, - Widget.Box( - { - hexpand: false, - vertical: true, - }, - Padding(name, { - vexpand: false, - className: 'event-top-padding', - }), - PopupRevealer(name, child, transition), - Padding(name, {} as Opts), - ), - Padding(name, {} as Opts), - ), - 'bottom-left': () => - Widget.Box( - {}, - Widget.Box( - { - hexpand: false, - vertical: true, - }, - Padding(name, {} as Opts), - PopupRevealer(name, child, transition), - ), - Padding(name, {} as Opts), - ), - 'bottom-center': () => - Widget.Box( - {}, - Padding(name, {} as Opts), - Widget.Box( - { - hexpand: false, - vertical: true, - }, - Padding(name, {} as Opts), - PopupRevealer(name, child, transition), - ), - Padding(name, {} as Opts), - ), - 'bottom-right': () => - Widget.Box( - {}, - Padding(name, {} as Opts), - Widget.Box( - { - hexpand: false, - vertical: true, - }, - Padding(name, {} as Opts), - PopupRevealer(name, child, transition), - ), - ), -}); - -const isValidLayout = (layout: string): layout is Layouts => { - return WINDOW_LAYOUTS.includes(layout); -}; - -export default ({ - name, - child, - layout = 'center', - transition = 'none', - exclusivity = 'ignore' as Exclusivity, - ...props -}: PopupWindowProps): Window => { - const layoutFn = isValidLayout(layout) ? layout : 'center'; - - const layoutWidget = Layout(name, child, transition)[layoutFn](); - - return Widget.Window({ - name, - class_names: [name, 'popup-window'], - setup: (w) => w.keybind('Escape', () => App.closeWindow(name)), - visible: false, - keymode: 'on-demand', - exclusivity, - layer: 'top', - anchor: ['top', 'bottom', 'right', 'left'], - child: layoutWidget, - ...props, - }); -}; diff --git a/modules/notifications/actions/index.ts b/modules/notifications/actions/index.ts deleted file mode 100644 index 402f1d9a8..000000000 --- a/modules/notifications/actions/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Attribute, Child } from 'lib/types/widget'; -import { Notification, Notifications } from 'types/service/notifications'; -import Box from 'types/widgets/box'; - -const Action = (notif: Notification, notifs: Notifications): Box => { - if (notif.actions !== undefined && notif.actions.length > 0) { - return Widget.Box({ - class_name: 'notification-card-actions', - hexpand: true, - vpack: 'end', - children: notif.actions.map((action) => { - return Widget.Button({ - hexpand: true, - class_name: 'notification-action-buttons', - on_primary_click: () => { - if (action.id.includes('scriptAction:-')) { - Utils.execAsync(`${action.id.replace('scriptAction:-', '')}`).catch((err) => - console.error(err), - ); - notifs.CloseNotification(notif.id); - } else { - notif.invoke(action.id); - } - }, - child: Widget.Box({ - hpack: 'center', - hexpand: true, - children: [ - Widget.Label({ - class_name: 'notification-action-buttons-label', - hexpand: true, - label: action.label, - max_width_chars: 15, - truncate: 'end', - wrap: true, - }), - ], - }), - }); - }), - }); - } - - return Widget.Box(); -}; - -export { Action }; diff --git a/modules/notifications/body/index.ts b/modules/notifications/body/index.ts deleted file mode 100644 index d6bc7fa65..000000000 --- a/modules/notifications/body/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Notification } from 'types/service/notifications'; -import { notifHasImg } from '../../menus/notifications/utils.js'; -import Box from 'types/widgets/box.js'; -import { Attribute, Child } from 'lib/types/widget.js'; - -export const Body = (notif: Notification): Box => { - return Widget.Box({ - vpack: 'start', - hexpand: true, - class_name: 'notification-card-body', - children: [ - Widget.Label({ - hexpand: true, - use_markup: true, - xalign: 0, - justification: 'left', - truncate: 'end', - lines: 2, - max_width_chars: !notifHasImg(notif) ? 35 : 28, - wrap: true, - class_name: 'notification-card-body-label', - label: notif['body'], - }).on('realize', (self) => { - self.set_markup(notif['body']); - }), - ], - }); -}; diff --git a/modules/notifications/close/index.ts b/modules/notifications/close/index.ts deleted file mode 100644 index d7a0d6e48..000000000 --- a/modules/notifications/close/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Attribute, Child } from 'lib/types/widget'; -import { Notification, Notifications } from 'types/service/notifications'; -import Button from 'types/widgets/button'; -import Label from 'types/widgets/label'; - -export const CloseButton = (notif: Notification, notifs: Notifications): Button, Attribute> => { - return Widget.Button({ - class_name: 'close-notification-button', - on_primary_click: () => { - notifs.CloseNotification(notif.id); - }, - child: Widget.Label({ - class_name: 'txt-icon notif-close', - label: '󰅜', - hpack: 'center', - }), - }); -}; diff --git a/modules/notifications/header/icon.ts b/modules/notifications/header/icon.ts deleted file mode 100644 index 5a02ca2b9..000000000 --- a/modules/notifications/header/icon.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Notification } from 'types/service/notifications.js'; -import { getNotificationIcon } from 'globals/notification.js'; -import Box from 'types/widgets/box'; -import { Attribute, Child } from 'lib/types/widget'; - -const NotificationIcon = ({ - app_entry = '', - app_icon = '', - app_name = '', -}: Partial): Box => { - return Widget.Box({ - css: ` - min-width: 2rem; - min-height: 2rem; - `, - child: Widget.Icon({ - class_name: 'notification-icon', - icon: getNotificationIcon(app_name, app_icon, app_entry), - }), - }); -}; - -export { NotificationIcon }; diff --git a/modules/notifications/header/index.ts b/modules/notifications/header/index.ts deleted file mode 100644 index 695291a3f..000000000 --- a/modules/notifications/header/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import GLib from 'gi://GLib'; -import { notifHasImg } from '../../menus/notifications/utils.js'; -import { NotificationIcon } from './icon.js'; -import { Notification } from 'types/service/notifications'; -import options from 'options.js'; -import Box from 'types/widgets/box.js'; -import { Attribute, Child } from 'lib/types/widget.js'; - -const { military } = options.menus.clock.time; - -export const Header = (notif: Notification): Box => { - const time = (time: number, format = '%I:%M %p'): string => { - return GLib.DateTime.new_from_unix_local(time).format(military.value ? '%H:%M' : format) || '--'; - }; - - return Widget.Box({ - vertical: false, - hexpand: true, - children: [ - Widget.Box({ - class_name: 'notification-card-header', - hpack: 'start', - children: [NotificationIcon(notif)], - }), - Widget.Box({ - class_name: 'notification-card-header', - hexpand: true, - hpack: 'start', - vpack: 'start', - children: [ - Widget.Label({ - class_name: 'notification-card-header-label', - hpack: 'start', - hexpand: true, - vexpand: true, - max_width_chars: !notifHasImg(notif) ? 30 : 19, - truncate: 'end', - wrap: true, - label: notif['summary'], - }).on('realize', (self) => { - self.set_markup(notif['summary']); - }), - ], - }), - Widget.Box({ - class_name: 'notification-card-header menu', - hpack: 'end', - vpack: 'start', - hexpand: true, - child: Widget.Label({ - vexpand: true, - class_name: 'notification-time', - label: time(notif.time), - }), - }), - ], - }); -}; diff --git a/modules/notifications/image/index.ts b/modules/notifications/image/index.ts deleted file mode 100644 index df1fdede2..000000000 --- a/modules/notifications/image/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Notification } from 'types/service/notifications'; -import { notifHasImg } from '../../menus/notifications/utils.js'; -import Box from 'types/widgets/box.js'; -import { Attribute, Child } from 'lib/types/widget.js'; - -const Image = (notif: Notification): Box => { - if (notifHasImg(notif)) { - return Widget.Box({ - class_name: 'notification-card-image-container', - hpack: 'center', - vpack: 'center', - vexpand: false, - child: Widget.Box({ - hpack: 'center', - vexpand: false, - class_name: 'notification-card-image', - css: `background-image: url("${notif.image}")`, - }), - }); - } - - return Widget.Box(); -}; - -export { Image }; diff --git a/modules/notifications/index.ts b/modules/notifications/index.ts deleted file mode 100644 index d373cd9da..000000000 --- a/modules/notifications/index.ts +++ /dev/null @@ -1,105 +0,0 @@ -const notifs = await Service.import('notifications'); -import options from 'options'; -import { notifHasImg } from '../menus/notifications/utils.js'; -import { Image } from './image/index.js'; -import { Action } from './actions/index.js'; -import { Header } from './header/index.js'; -import { Body } from './body/index.js'; -import { CloseButton } from './close/index.js'; -import { getPosition } from 'lib/utils.js'; -import { filterNotifications } from 'lib/shared/notifications.js'; -import { Notification } from 'types/service/notifications.js'; -import Window from 'types/widgets/window.js'; -import Box from 'types/widgets/box.js'; -import { Attribute, Child } from 'lib/types/widget.js'; -const hyprland = await Service.import('hyprland'); - -const { position, timeout, cache_actions, monitor, active_monitor, displayedTotal, ignore, showActionsOnHover } = - options.notifications; - -const curMonitor = Variable(monitor.value); - -hyprland.active.connect('changed', () => { - curMonitor.value = hyprland.active.monitor.id; -}); - -export default (): Window, unknown> => { - Utils.merge([timeout.bind('value'), cache_actions.bind('value')], (timeout, doCaching) => { - notifs.popupTimeout = timeout; - notifs.cacheActions = doCaching; - }); - - return Widget.Window({ - name: 'notifications-window', - class_name: 'notifications-window', - monitor: Utils.merge( - [curMonitor.bind('value'), monitor.bind('value'), active_monitor.bind('value')], - (curMon, mon, activeMonitor) => { - if (activeMonitor === true) { - return curMon; - } - - return mon; - }, - ), - layer: options.tear.bind('value').as((tear) => (tear ? 'top' : 'overlay')), - anchor: position.bind('value').as((v) => getPosition(v)), - exclusivity: 'normal', - child: Widget.Box({ - class_name: 'notification-card-container', - vertical: true, - hexpand: true, - setup: (self) => { - Utils.merge( - [notifs.bind('popups'), ignore.bind('value'), showActionsOnHover.bind('value')], - (notifications: Notification[], ignoredNotifs: string[], showActions: boolean) => { - const filteredNotifications = filterNotifications(notifications, ignoredNotifs); - - return (self.children = filteredNotifications.slice(0, displayedTotal.value).map((notif) => { - const actionsbox = - notif.actions.length > 0 - ? Widget.Revealer({ - transition: 'slide_down', - reveal_child: showActions ? false : true, - child: Widget.EventBox({ - child: Action(notif, notifs), - }), - }) - : null; - - return Widget.EventBox({ - on_secondary_click: () => { - notifs.CloseNotification(notif.id); - }, - on_hover() { - if (actionsbox && showActions) actionsbox.reveal_child = true; - }, - on_hover_lost() { - if (actionsbox && showActions) actionsbox.reveal_child = false; - }, - child: Widget.Box({ - class_name: 'notification-card', - vpack: 'start', - hexpand: true, - children: [ - Image(notif), - Widget.Box({ - vpack: 'start', - vertical: true, - hexpand: true, - class_name: `notification-card-content ${!notifHasImg(notif) ? 'noimg' : ''}`, - children: actionsbox - ? [Header(notif), Body(notif), actionsbox] - : [Header(notif), Body(notif)], - }), - CloseButton(notif, notifs), - ], - }), - }); - })); - }, - ); - }, - }), - }); -}; diff --git a/modules/osd/bar/index.ts b/modules/osd/bar/index.ts deleted file mode 100644 index b73cda1fe..000000000 --- a/modules/osd/bar/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { OSDOrientation } from 'lib/types/options'; -import brightness from 'services/Brightness'; -import options from 'options'; -import Box from 'types/widgets/box'; -import { Attribute, Child } from 'lib/types/widget'; -const audio = await Service.import('audio'); - -export const OSDBar = (ort: OSDOrientation): Box => { - return Widget.Box({ - class_name: 'osd-bar-container', - children: [ - Widget.LevelBar({ - class_name: 'osd-bar', - vertical: ort === 'vertical', - inverted: ort === 'vertical', - bar_mode: 'continuous', - setup: (self) => { - self.hook( - brightness, - () => { - self.class_names = self.class_names.filter((c) => c !== 'overflow'); - self.value = brightness.screen; - }, - 'notify::screen', - ); - self.hook( - brightness, - () => { - self.class_names = self.class_names.filter((c) => c !== 'overflow'); - self.value = brightness.kbd; - }, - 'notify::kbd', - ); - self.hook( - audio.microphone, - () => { - self.toggleClassName('overflow', audio.microphone.volume > 1); - self.value = - audio.microphone.volume <= 1 ? audio.microphone.volume : audio.microphone.volume - 1; - }, - 'notify::volume', - ); - self.hook( - audio.microphone, - () => { - self.toggleClassName( - 'overflow', - audio.microphone.volume > 1 && - (!options.theme.osd.muted_zero.value || audio.microphone.is_muted === false), - ); - self.value = - options.theme.osd.muted_zero.value && audio.microphone.is_muted !== false - ? 0 - : audio.microphone.volume <= 1 - ? audio.microphone.volume - : audio.microphone.volume - 1; - }, - 'notify::is-muted', - ); - self.hook( - audio.speaker, - () => { - self.toggleClassName('overflow', audio.speaker.volume > 1); - self.value = audio.speaker.volume <= 1 ? audio.speaker.volume : audio.speaker.volume - 1; - }, - 'notify::volume', - ); - self.hook( - audio.speaker, - () => { - self.toggleClassName( - 'overflow', - audio.speaker.volume > 1 && - (!options.theme.osd.muted_zero.value || audio.speaker.is_muted === false), - ); - self.value = - options.theme.osd.muted_zero.value && audio.speaker.is_muted !== false - ? 0 - : audio.speaker.volume <= 1 - ? audio.speaker.volume - : audio.speaker.volume - 1; - }, - 'notify::is-muted', - ); - }, - }), - ], - }); -}; diff --git a/modules/osd/icon/index.ts b/modules/osd/icon/index.ts deleted file mode 100644 index 1682c91eb..000000000 --- a/modules/osd/icon/index.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Attribute, Child } from 'lib/types/widget'; -import brightness from 'services/Brightness'; -import Box from 'types/widgets/box'; -const audio = await Service.import('audio'); - -export const OSDIcon = (): Box => { - return Widget.Box({ - class_name: 'osd-icon-container', - hexpand: true, - child: Widget.Label({ - class_name: 'osd-icon txt-icon', - hexpand: true, - vexpand: true, - hpack: 'center', - vpack: 'center', - setup: (self) => { - self.hook( - brightness, - () => { - self.label = '󱍖'; - }, - 'notify::screen', - ); - self.hook( - brightness, - () => { - self.label = '󰥻'; - }, - 'notify::kbd', - ); - self.hook( - audio.microphone, - () => { - self.label = audio.microphone.is_muted ? '󰍭' : '󰍬'; - }, - 'notify::volume', - ); - self.hook( - audio.microphone, - () => { - self.label = audio.microphone.is_muted ? '󰍭' : '󰍬'; - }, - 'notify::is-muted', - ); - self.hook( - audio.speaker, - () => { - self.label = audio.speaker.is_muted ? '󰝟' : '󰕾'; - }, - 'notify::volume', - ); - self.hook( - audio.speaker, - () => { - self.label = audio.speaker.is_muted ? '󰝟' : '󰕾'; - }, - 'notify::is-muted', - ); - }, - }), - }); -}; diff --git a/modules/osd/index.ts b/modules/osd/index.ts deleted file mode 100644 index 8735ebd88..000000000 --- a/modules/osd/index.ts +++ /dev/null @@ -1,200 +0,0 @@ -import options from 'options'; -import brightness from 'services/Brightness'; -import { OSDLabel } from './label/index'; -import { OSDBar } from './bar/index'; -import { OSDIcon } from './icon/index'; -import { getPosition } from 'lib/utils'; -import { Attribute, Child } from 'lib/types/widget'; -import { Revealer } from 'resource:///com/github/Aylur/ags/widgets/revealer.js'; -import { Window } from 'resource:///com/github/Aylur/ags/widgets/window.js'; -const hyprland = await Service.import('hyprland'); -const audio = await Service.import('audio'); - -const { enable, duration, orientation, location, active_monitor, monitor } = options.theme.osd; - -const curMonitor = Variable(monitor.value); - -hyprland.active.connect('changed', () => { - curMonitor.value = hyprland.active.monitor.id; -}); - -let count = 0; - -const handleRevealRevealer = (self: Revealer, property: 'reveal_child' | 'visible'): void => { - if (!enable.value || property !== 'reveal_child') { - return; - } - - self.reveal_child = true; - - count++; - Utils.timeout(duration.value, () => { - count--; - - if (count === 0) { - self.reveal_child = false; - } - }); -}; - -const handleRevealWindow = (self: Window, property: 'reveal_child' | 'visible'): void => { - if (!enable.value || property !== 'visible') { - return; - } - - self.visible = true; - - count++; - Utils.timeout(duration.value, () => { - count--; - - if (count === 0) { - self.visible = false; - } - }); -}; - -const handleReveal = ( - self: Revealer | Window, - property: 'reveal_child' | 'visible', -): void => { - if (self instanceof Revealer) { - handleRevealRevealer(self, property); - } else if (self instanceof Window) { - handleRevealWindow(self, property); - } -}; - -const renderOSD = (): Revealer => { - return Widget.Revealer({ - transition: 'crossfade', - reveal_child: false, - setup: (self) => { - self.hook( - brightness, - () => { - handleReveal(self, 'reveal_child'); - }, - 'notify::screen', - ); - self.hook( - brightness, - () => { - handleReveal(self, 'reveal_child'); - }, - 'notify::kbd', - ); - self.hook( - audio.microphone, - () => { - handleReveal(self, 'reveal_child'); - }, - 'notify::volume', - ); - self.hook( - audio.microphone, - () => { - handleReveal(self, 'reveal_child'); - }, - 'notify::is-muted', - ); - self.hook( - audio.speaker, - () => { - handleReveal(self, 'reveal_child'); - }, - 'notify::volume', - ); - self.hook( - audio.speaker, - () => { - handleReveal(self, 'reveal_child'); - }, - 'notify::is-muted', - ); - }, - child: Widget.Box({ - class_name: 'osd-container', - vertical: orientation.bind('value').as((ort) => ort === 'vertical'), - children: orientation.bind('value').as((ort) => { - if (ort === 'vertical') { - return [OSDLabel(), OSDBar(ort), OSDIcon()]; - } - - return [OSDIcon(), OSDBar(ort), OSDLabel()]; - }), - }), - }); -}; - -export default (): Window => - Widget.Window({ - monitor: Utils.merge( - [curMonitor.bind('value'), monitor.bind('value'), active_monitor.bind('value')], - (curMon, mon, activeMonitor) => { - if (activeMonitor === true) { - return curMon; - } - - return mon; - }, - ), - name: `indicator`, - class_name: 'indicator', - visible: false, - layer: options.tear.bind('value').as((tear) => (tear ? 'top' : 'overlay')), - anchor: location.bind('value').as((v) => getPosition(v)), - click_through: true, - child: Widget.Box({ - css: 'padding: 1px;', - expand: true, - child: renderOSD(), - }), - setup: (self) => { - self.hook(enable, () => { - handleReveal(self, 'visible'); - }); - self.hook( - brightness, - () => { - handleReveal(self, 'visible'); - }, - 'notify::screen', - ); - self.hook( - brightness, - () => { - handleReveal(self, 'visible'); - }, - 'notify::kbd', - ); - self.hook( - audio.microphone, - () => { - handleReveal(self, 'visible'); - }, - 'notify::volume', - ); - self.hook( - audio.microphone, - () => { - handleReveal(self, 'visible'); - }, - 'notify::is-muted', - ); - self.hook( - audio.speaker, - () => { - handleReveal(self, 'visible'); - }, - 'notify::volume', - ); - self.hook( - audio.speaker, - () => { - handleReveal(self, 'visible'); - }, - 'notify::is-muted', - ); - }, - }); diff --git a/modules/osd/label/index.ts b/modules/osd/label/index.ts deleted file mode 100644 index 9e61636e7..000000000 --- a/modules/osd/label/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import brightness from 'services/Brightness'; -import options from 'options'; -import Box from 'types/widgets/box'; -import { Attribute, Child } from 'lib/types/widget'; -const audio = await Service.import('audio'); - -export const OSDLabel = (): Box => { - return Widget.Box({ - class_name: 'osd-label-container', - hexpand: true, - vexpand: true, - child: Widget.Label({ - class_name: 'osd-label', - hexpand: true, - vexpand: true, - hpack: 'center', - vpack: 'center', - setup: (self) => { - self.hook( - brightness, - () => { - self.class_names = self.class_names.filter((c) => c !== 'overflow'); - self.label = `${Math.round(brightness.screen * 100)}`; - }, - 'notify::screen', - ); - self.hook( - brightness, - () => { - self.class_names = self.class_names.filter((c) => c !== 'overflow'); - self.label = `${Math.round(brightness.kbd * 100)}`; - }, - 'notify::kbd', - ); - self.hook( - audio.microphone, - () => { - self.toggleClassName('overflow', audio.microphone.volume > 1); - self.label = `${Math.round(audio.microphone.volume * 100)}`; - }, - 'notify::volume', - ); - self.hook( - audio.microphone, - () => { - self.toggleClassName( - 'overflow', - audio.microphone.volume > 1 && - (!options.theme.osd.muted_zero.value || audio.microphone.is_muted === false), - ); - const inputVolume = - options.theme.osd.muted_zero.value && audio.microphone.is_muted !== false - ? 0 - : Math.round(audio.microphone.volume * 100); - self.label = `${inputVolume}`; - }, - 'notify::is-muted', - ); - self.hook( - audio.speaker, - () => { - self.toggleClassName('overflow', audio.speaker.volume > 1); - self.label = `${Math.round(audio.speaker.volume * 100)}`; - }, - 'notify::volume', - ); - self.hook( - audio.speaker, - () => { - self.toggleClassName( - 'overflow', - audio.speaker.volume > 1 && - (!options.theme.osd.muted_zero.value || audio.speaker.is_muted === false), - ); - const speakerVolume = - options.theme.osd.muted_zero.value && audio.speaker.is_muted !== false - ? 0 - : Math.round(audio.speaker.volume * 100); - self.label = `${speakerVolume}`; - }, - 'notify::is-muted', - ); - }, - }), - }); -}; diff --git a/modules/shared/barItemBox.ts b/modules/shared/barItemBox.ts deleted file mode 100644 index 0f84c41a4..000000000 --- a/modules/shared/barItemBox.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BarBoxChild } from 'lib/types/bar'; -import { Bind } from 'lib/types/variable'; -import { Attribute, GtkWidget } from 'lib/types/widget'; -import options from 'options'; -import Button from 'types/widgets/button'; - -export const BarItemBox = (child: BarBoxChild): Button => { - const computeVisible = (): Bind | boolean => { - if (child.isVis !== undefined) { - return child.isVis.bind('value'); - } - return child.isVisible; - }; - - return Widget.Button({ - class_name: options.theme.bar.buttons.style.bind('value').as((style) => { - const styleMap = { - default: 'style1', - split: 'style2', - wave: 'style3', - wave2: 'style4', - }; - - const boxClassName = Object.hasOwnProperty.call(child, 'boxClass') ? child.boxClass : ''; - - return `bar_item_box_visible ${styleMap[style]} ${boxClassName}`; - }), - child: child.component, - visible: computeVisible(), - ...child.props, - }); -}; diff --git a/services/bluetooth.py b/scripts/bluetooth.py similarity index 100% rename from services/bluetooth.py rename to scripts/bluetooth.py diff --git a/scripts/hyprpanel_launcher.sh.in b/scripts/hyprpanel_launcher.sh.in new file mode 100644 index 000000000..122d53c2f --- /dev/null +++ b/scripts/hyprpanel_launcher.sh.in @@ -0,0 +1,9 @@ +#!/bin/sh + +export HYPRPANEL_DATADIR="@DATADIR@" + +if [ "$#" -eq 0 ]; then + exec gjs -m "@DATADIR@/hyprpanel.js" +else + exec astal -i hyprpanel "$@" +fi diff --git a/install_fonts.sh b/scripts/install_fonts.sh similarity index 100% rename from install_fonts.sh rename to scripts/install_fonts.sh diff --git a/services/screen_record.sh b/scripts/screen_record.sh similarity index 100% rename from services/screen_record.sh rename to scripts/screen_record.sh diff --git a/services/snapshot.sh b/scripts/snapshot.sh similarity index 100% rename from services/snapshot.sh rename to scripts/snapshot.sh diff --git a/scss/optionsTrackers.ts b/scss/optionsTrackers.ts deleted file mode 100644 index ef360de59..000000000 --- a/scss/optionsTrackers.ts +++ /dev/null @@ -1,58 +0,0 @@ -import icons from 'lib/icons'; -import { bash, dependencies, Notify, isAnImage } from 'lib/utils'; -import options from 'options'; -import Wallpaper from 'services/Wallpaper'; - -const { matugen } = options.theme; -const { mode, scheme_type, contrast } = options.theme.matugen_settings; - -const ensureMatugenWallpaper = (): void => { - const wallpaperPath = options.wallpaper.image.value; - - if (matugen.value && (!options.wallpaper.image.value.length || !isAnImage(wallpaperPath))) { - Notify({ - summary: 'Matugen Failed', - body: "Please select a wallpaper in 'Theming > General' first.", - iconName: icons.ui.warning, - timeout: 7000, - }); - matugen.value = false; - } -}; - -export const initializeTrackers = (resetCssFunc: () => void): void => { - matugen.connect('changed', () => { - ensureMatugenWallpaper(); - options.resetTheme(); - }); - - mode.connect('changed', () => { - options.resetTheme(); - }); - scheme_type.connect('changed', () => { - options.resetTheme(); - }); - contrast.connect('changed', () => { - options.resetTheme(); - }); - - Wallpaper.connect('changed', () => { - console.info('Wallpaper changed, regenerating Matugen colors...'); - if (options.theme.matugen.value) { - options.resetTheme(); - resetCssFunc(); - } - }); - - options.wallpaper.image.connect('changed', () => { - if ((!Wallpaper.isRunning() && options.theme.matugen.value) || !options.wallpaper.enable.value) { - console.info('Wallpaper path changed, regenerating Matugen colors...'); - options.resetTheme(); - resetCssFunc(); - } - if (options.wallpaper.pywal.value && dependencies('wal')) { - const wallpaperPath = options.wallpaper.image.value; - bash(`wal -i ${wallpaperPath}`); - } - }); -}; diff --git a/scss/style.ts b/scss/style.ts deleted file mode 100644 index 06795066e..000000000 --- a/scss/style.ts +++ /dev/null @@ -1,79 +0,0 @@ -import options from 'options'; -import { bash, dependencies } from 'lib/utils'; -import { MatugenColors, RecursiveOptionsObject } from 'lib/types/options'; -import { initializeTrackers } from './optionsTrackers'; -import { generateMatugenColors, replaceHexValues } from '../services/matugen/index'; -import { isHexColor } from 'globals/variables'; - -const deps = ['font', 'theme', 'bar.flatButtons', 'bar.position', 'bar.battery.charging', 'bar.battery.blocks']; - -function extractVariables(theme: RecursiveOptionsObject, prefix = '', matugenColors?: MatugenColors): string[] { - let result = [] as string[]; - for (const key in theme) { - if (!theme.hasOwnProperty(key)) { - continue; - } - - const themeValue = theme[key]; - - const newPrefix = prefix ? `${prefix}-${key}` : key; - - const replacedValue = - isHexColor(themeValue.value) && matugenColors !== undefined - ? replaceHexValues(themeValue.value, matugenColors) - : themeValue.value; - - if (typeof themeValue === 'function') { - result.push(`$${newPrefix}: ${replacedValue};`); - continue; - } - if (typeof themeValue !== 'object' || themeValue === null || Array.isArray(themeValue)) continue; - - if (typeof themeValue.value !== 'undefined') { - result.push(`$${newPrefix}: ${replacedValue};`); - } else { - result = result.concat(extractVariables(themeValue, newPrefix, matugenColors)); - } - } - - return result; -} - -const resetCss = async (): Promise => { - if (!dependencies('sass')) return; - - try { - const matugenColors = await generateMatugenColors(); - - const variables = extractVariables(options.theme as RecursiveOptionsObject, '', matugenColors); - - const vars = `${TMP}/variables.scss`; - const css = `${TMP}/main.css`; - const scss = `${TMP}/entry.scss`; - const localScss = `${App.configDir}/scss/main.scss`; - - const themeVariables = variables; - const integratedVariables = themeVariables; - - const imports = [vars].map((f) => `@import '${f}';`); - - await Utils.writeFile(integratedVariables.join('\n'), vars); - - let mainScss = Utils.readFile(localScss); - mainScss = `${imports}\n${mainScss}`; - - await Utils.writeFile(mainScss, scss); - - await bash(`sass --load-path=${App.configDir}/scss/ ${scss} ${css}`); - - App.applyCss(css, true); - } catch (error) { - console.error(error); - } -}; - -initializeTrackers(resetCss); - -Utils.monitorFile(`${App.configDir}/scss/style`, resetCss); -options.handler(deps, resetCss); -await resetCss(); diff --git a/scss/style/menus/audiomenu.scss b/scss/style/menus/audiomenu.scss deleted file mode 100644 index 767b30abf..000000000 --- a/scss/style/menus/audiomenu.scss +++ /dev/null @@ -1,185 +0,0 @@ -.menu-items-container.audio { - min-width: 18em * $bar-menus-menu-volume-scaling * 0.01; - - @import "./menu.scss"; - - * { - font-size: $font-size * $bar-menus-menu-volume-scaling * 0.01; - } - - background: if($bar-menus-monochrome, $bar-menus-background, $bar-menus-menu-volume-background-color); - - .menu-items { - border-color: if($bar-menus-monochrome, $bar-menus-border-color, $bar-menus-menu-volume-border-color); - opacity: $bar-menus-opacity * 0.01; - } - - .menu-dropdown-label.audio { - color: if($bar-menus-monochrome, $bar-menus-label, $bar-menus-menu-volume-label-color); - } - - .menu-label.audio { - color: if($bar-menus-monochrome, $bar-menus-label, $bar-menus-menu-volume-label-color); - } - - .menu-active.playback, - .menu-active.input { - color: if($bar-menus-monochrome, $bar-menus-text, $bar-menus-menu-volume-text); - } - - .menu-button-isactive.audio { - color: if($bar-menus-monochrome, $bar-menus-icons-active, $bar-menus-menu-volume-icons-active); - } - - .menu-slider.playback { - trough { - background: if($bar-menus-monochrome, $bar-menus-slider-background, $bar-menus-menu-volume-audio_slider-background); - - highlight, - progress { - background: if($bar-menus-monochrome, $bar-menus-slider-primary, $bar-menus-menu-volume-audio_slider-primary); - } - } - - &:hover { - trough { - background: if($bar-menus-monochrome, $bar-menus-slider-backgroundhover, $bar-menus-menu-volume-audio_slider-backgroundhover); - } - - slider { - background: if($bar-menus-monochrome, $bar-menus-slider-puck, $bar-menus-menu-volume-audio_slider-puck); - } - } - } - - .menu-slider.inputs { - trough { - background: if($bar-menus-monochrome, $bar-menus-slider-background, $bar-menus-menu-volume-input_slider-background); - - highlight, - progress { - background: if($bar-menus-monochrome, $bar-menus-slider-primary, $bar-menus-menu-volume-input_slider-primary); - } - } - - &:hover { - trough { - background: if($bar-menus-monochrome, $bar-menus-slider-backgroundhover, $bar-menus-menu-volume-input_slider-backgroundhover); - } - - slider { - background: if($bar-menus-monochrome, $bar-menus-slider-puck, $bar-menus-menu-volume-input_slider-puck); - } - } - } - - .menu-active-percentage.playback, - .menu-active-percentage.input { - color: if($bar-menus-monochrome, $bar-menus-text, $bar-menus-menu-volume-text); - } - - .menu-active-button { - - .menu-active-icon.playback, - .menu-active-icon.input { - color: if($bar-menus-monochrome, $bar-menus-iconbuttons-active, $bar-menus-menu-volume-iconbutton-active); - opacity: 1; - } - - &.muted { - - .menu-active-icon.playback, - .menu-active-icon.input { - color: if($bar-menus-monochrome, $bar-menus-iconbuttons-passive, $bar-menus-menu-volume-iconbutton-passive); - opacity: 1; - } - } - - &:hover { - - .menu-active-icon.playback, - .menu-active-icon.input { - color: if($bar-menus-monochrome, $bar-menus-iconbuttons-passive, $bar-menus-menu-volume-iconbutton-passive); - opacity: 0.3; - } - } - - &.muted:hover { - - .menu-active-icon.playback, - .menu-active-icon.input { - color: if($bar-menus-monochrome, $bar-menus-iconbuttons-active, $bar-menus-menu-volume-iconbutton-active); - opacity: 1; - } - } - } - - .menu-button-icon.playback, - .menu-button-icon.input { - color: if($bar-menus-monochrome, $bar-menus-icons-passive, $bar-menus-menu-volume-icons-passive); - - &.active { - color: if($bar-menus-monochrome, $bar-menus-icons-active, $bar-menus-menu-volume-icons-active); - } - } - - .menu-button.audio { - color: if($bar-menus-monochrome, $bar-menus-icons-passive, $bar-menus-menu-volume-icons-passive); - - .menu-button-name.playback, - .menu-button-name.input { - color: if($bar-menus-monochrome, $bar-menus-text, $bar-menus-menu-volume-text); - } - - - &:hover { - - .menu-button-name.playback, - .menu-button-name.input { - color: if($bar-menus-monochrome, $bar-menus-listitems-active, $bar-menus-menu-volume-listitems-active); - } - } - } - - .menu-section-container.volume { - margin-bottom: 0.65em; - } - - .menu-section-container.playback { - margin-top: 0em; - margin-bottom: 1em; - } - - .menu-section-container.input { - margin-top: 0em; - } - - .menu-label-container.input { - border-radius: 0em; - background: if($bar-menus-monochrome, $bar-menus-cards, $bar-menus-menu-volume-card-color); - } - - .menu-label-container.playback { - background: if($bar-menus-monochrome, $bar-menus-cards, $bar-menus-menu-volume-card-color); - } - - .menu-items-section.input { - background: if($bar-menus-monochrome, $bar-menus-cards, $bar-menus-menu-volume-card-color); - } - - .menu-items-section.playback { - background: if($bar-menus-monochrome, $bar-menus-cards, $bar-menus-menu-volume-card-color); - } - - .menu-label-container.selected { - background: if($bar-menus-monochrome, $bar-menus-cards, $bar-menus-menu-volume-card-color); - } - - .menu-items-section.selected { - background: if($bar-menus-monochrome, $bar-menus-cards, $bar-menus-menu-volume-card-color); - } - - .menu-items-section.playback { - border-radius: 0em; - } -} diff --git a/scss/style/menus/power.scss b/scss/style/menus/power.scss deleted file mode 100644 index 43c1c035f..000000000 --- a/scss/style/menus/power.scss +++ /dev/null @@ -1,227 +0,0 @@ -window#powermenu, -window#verification { - // the fraction has to be more than hyprland ignorealpha - background-color: rgba(0, 0, 0, .4); -} - -$popover-padding: 0.6rem * 1.6; - -window#verification .verification { - * { - font-size: $font-size * $bar-menus-menu-dashboard-confirmation_scaling * 0.01; - } - - @include floating-widget; - background: if($bar-menus-monochrome, $bar-menus-background, $bar-menus-menu-dashboard-powermenu-confirmation-background); - border: $bar-menus-border-size solid if($bar-menus-monochrome, $bar-menus-border-color, $bar-menus-menu-dashboard-powermenu-confirmation-border); - padding: 0.35em * 1.6 * 1.5; - min-width: 20em; - min-height: 6em; - opacity: $bar-menus-opacity * 0.01; - - .verification-content { - background: if($bar-menus-monochrome, $bar-menus-cards, $bar-menus-menu-dashboard-powermenu-confirmation-card); - border-radius: $bar-menus-border-radius * 0.5; - padding: 1em; - } - - .text-box { - margin-bottom: 0.3em; - - .title { - font-size: 1.5em; - color: if($bar-menus-monochrome, $bar-menus-label, $bar-menus-menu-dashboard-powermenu-confirmation-label); - margin-bottom: 0.5em; - } - - .desc { - color: if($bar-menus-monochrome, $bar-menus-text, $bar-menus-menu-dashboard-powermenu-confirmation-body); - font-size: 1em; - margin-bottom: 0.55em; - padding: 1em 3em; - } - } - - .verification-button { - background: $bar-menus-buttons-default; - padding: 0.7em 0em; - margin: 0.4em 1.7em; - border-radius: $bar-menus-border-radius * 0.5; - opacity: 1; - transition: border-color 0.2s ease-in-out; - transition: opacity .3s ease-in-out; - - &.bar-verification_yes { - background-color: if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-confirm); - } - - &.bar-verification_no { - background-color: if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-deny); - } - - &:hover { - &.bar-verification_yes { - background-color: transparentize(if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-confirm), 0.6); - transition: background-color 0.2s ease-in-out; - } - - &.bar-verification_no { - background-color: transparentize(if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-deny), 0.6); - transition: background-color 0.2s ease-in-out; - } - } - - &:focus { - &.bar-verification_yes { - background-color: transparentize(if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-confirm), 0.6); - transition: background 0.2s ease-in-out; - } - - &.bar-verification_no { - background-color: transparentize(if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-deny), 0.6); - transition: background 0.2s ease-in-out; - } - } - - &:active { - &.bar-verification_yes { - background-color: transparentize(if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-confirm), 0.6); - transition: background 0.2s ease-in-out; - } - - &.bar-verification_no { - background-color: transparentize(if($bar-menus-monochrome, $bar-menus-buttons-default, $bar-menus-menu-dashboard-powermenu-confirmation-deny), 0.6); - transition: background 0.2s ease-in-out; - } - - image { - opacity: .3; - transition: opacity .3s ease-in-out; - } - - label { - opacity: .3; - transition: opacity .3s ease-in-out; - } - } - } - - .bar-verification_no label { - color: if($bar-menus-monochrome, $bar-menus-buttons-text, $bar-menus-menu-dashboard-powermenu-confirmation-button_text); - } - - .bar-verification_yes label { - color: if($bar-menus-monochrome, $bar-menus-buttons-text, $bar-menus-menu-dashboard-powermenu-confirmation-button_text); - } -} - -window#powermenu .powermenu { - @include floating-widget; - - &.line { - padding: $popover-padding * 1.5; - } - - &.box { - padding: $popover-padding * 2; - } -} - -.widget-button { - border-color: $crust; - min-width: 4.5em; - min-height: 4.5em; - opacity: 1; - transition: border-color 0.2s ease-in-out; - transition: opacity .3s ease-in-out; - - &:hover { - &.powermenu-button-shutdown { - border-color: $red; - } - - &.powermenu-button-logout { - border-color: $green; - } - - &.powermenu-button-sleep { - border-color: $sky; - } - - &.powermenu-button-reboot { - border-color: $peach; - } - } - - &:focus { - &.powermenu-button-shutdown { - border-color: $red; - } - - &.powermenu-button-logout { - border-color: $green; - } - - &.powermenu-button-sleep { - border-color: $sky; - } - - &.powermenu-button-reboot { - border-color: $peach; - } - } - - &:active { - &.powermenu-button-shutdown { - border-color: rgba($red, .5); - } - - &.powermenu-button-logout { - border-color: rgba($green, .5); - } - - &.powermenu-button-sleep { - border-color: rgba($sky, .5); - } - - &.powermenu-button-reboot { - border-color: rgba($peach, .5); - } - } -} - -.system-button_icon { - &.shutdown { - color: $red; - } - - &.logout { - color: $green; - } - - &.reboot { - color: $peach; - } - - &.sleep { - color: $sky; - } -} - -.system-button_label { - &.shutdown { - color: $red; - } - - &.logout { - color: $green; - } - - &.reboot { - color: $peach; - } - - &.sleep { - color: $sky; - } -} diff --git a/services/Brightness.ts b/services/Brightness.ts deleted file mode 100644 index 4ac8d49b7..000000000 --- a/services/Brightness.ts +++ /dev/null @@ -1,74 +0,0 @@ -// <3 Aylur for this brightness service -import { bash, dependencies, sh } from 'lib/utils'; - -if (!dependencies('brightnessctl')) App.quit(); - -const get = (args: string): number => Number(Utils.exec(`brightnessctl ${args}`)); -const screen = await bash`ls -w1 /sys/class/backlight | head -1`; -const kbd = await bash`ls -w1 /sys/class/leds | grep '::kbd_backlight$' | head -1`; - -class Brightness extends Service { - static { - Service.register( - this, - {}, - { - screen: ['float', 'rw'], - kbd: ['int', 'rw'], - }, - ); - } - - #kbdMax = get(`--device ${kbd} max`); - #kbd = get(`--device ${kbd} get`); - #screenMax = get(`--device ${screen} max`); - #screen = get(`--device ${screen} get`) / (get(`--device ${screen} max`) || 1); - - get kbd(): number { - return this.#kbd; - } - get screen(): number { - return this.#screen; - } - - set kbd(value) { - if (value < 0 || value > this.#kbdMax) return; - - sh(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { - this.#kbd = value; - this.changed('kbd'); - }); - } - - set screen(percent) { - if (percent < 0) percent = 0; - - if (percent > 1) percent = 1; - - sh(`brightnessctl set ${Math.round(percent * 100)}% -d ${screen} -q`).then(() => { - this.#screen = percent; - this.changed('screen'); - }); - } - - constructor() { - super(); - - const screenPath = `/sys/class/backlight/${screen}/brightness`; - const kbdPath = `/sys/class/leds/${kbd}/brightness`; - - Utils.monitorFile(screenPath, async (f) => { - const v = await Utils.readFileAsync(f); - this.#screen = Number(v) / this.#screenMax; - this.changed('screen'); - }); - - Utils.monitorFile(kbdPath, async (f) => { - const v = await Utils.readFileAsync(f); - this.#kbd = Number(v) / this.#kbdMax; - this.changed('kbd'); - }); - } -} - -export default new Brightness(); diff --git a/services/Wallpaper.ts b/services/Wallpaper.ts deleted file mode 100644 index a27947cdd..000000000 --- a/services/Wallpaper.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { dependencies, sh } from 'lib/utils'; -import options from 'options'; -const hyprland = await Service.import('hyprland'); - -const WP = `${Utils.HOME}/.config/background`; - -class Wallpaper extends Service { - static { - Service.register( - this, - {}, - { - wallpaper: ['string'], - }, - ); - } - - #blockMonitor = false; - #isRunning = false; - - #wallpaper(): void { - if (!dependencies('swww')) return; - - hyprland.monitors.map((m) => m.name); - sh('hyprctl cursorpos').then((pos) => { - sh([ - 'swww', - 'img', - '--invert-y', - '--transition-type', - 'grow', - '--transition-duration', - '1.5', - '--transition-fps', - '30', - '--transition-pos', - pos.replace(' ', ''), - WP, - ]).then(() => { - this.changed('wallpaper'); - }); - }); - } - - async #setWallpaper(path: string): Promise { - this.#blockMonitor = true; - - await sh(`cp ${path} ${WP}`); - this.#wallpaper(); - - this.#blockMonitor = false; - } - - readonly set = (path: string): void => { - this.#setWallpaper(path); - }; - readonly isRunning = (): boolean => { - return this.#isRunning; - }; - - get wallpaper(): string { - return WP; - } - - constructor() { - super(); - - options.wallpaper.enable.connect('changed', () => { - if (options.wallpaper.enable.value) { - this.#isRunning = true; - Utils.execAsync('swww-daemon') - .then(() => { - this.#wallpaper(); - }) - .catch(() => null); - } else { - this.#isRunning = false; - Utils.execAsync('pkill swww-daemon').catch(() => null); - } - }); - - if (!dependencies('swww') || !options.wallpaper.enable.value) return this; - - this.#isRunning = true; - Utils.monitorFile(WP, () => { - if (!this.#blockMonitor) this.#wallpaper(); - }); - - Utils.execAsync('swww-daemon') - .then(() => { - this.#wallpaper(); - }) - .catch(() => null); - } -} - -export default new Wallpaper(); diff --git a/src/cli/commander/InitializeCommand.ts b/src/cli/commander/InitializeCommand.ts new file mode 100644 index 000000000..6567dee79 --- /dev/null +++ b/src/cli/commander/InitializeCommand.ts @@ -0,0 +1,19 @@ +import { CommandRegistry } from './Registry'; +import { Command } from './types'; +import { createExplainCommand } from './helpers'; +import { appearanceCommands } from './commands/appearance'; +import { utilityCommands } from './commands/utility'; +import { windowManagementCommands } from './commands/windowManagement'; + +/** + * Initializes and registers commands in the provided CommandRegistry. + * + * @param registry - The command registry to register commands in. + */ +export function initializeCommands(registry: CommandRegistry): void { + const commandList: Command[] = [...appearanceCommands, ...utilityCommands, ...windowManagementCommands]; + + commandList.forEach((command) => registry.register(command)); + + registry.register(createExplainCommand(registry)); +} diff --git a/src/cli/commander/Parser.ts b/src/cli/commander/Parser.ts new file mode 100644 index 000000000..9645b0ac2 --- /dev/null +++ b/src/cli/commander/Parser.ts @@ -0,0 +1,143 @@ +import { CommandRegistry } from './Registry'; +import { Command, ParsedCommand } from './types'; + +/** + * The CommandParser is responsible for parsing the input string into a command and its positional arguments. + * It does not handle flags, only positional arguments. + * + * Expected command format: + * astal arg1 arg2 arg3... + * + * The parser: + * 1. Tokenizes the input. + * 2. Identifies the command by the first token. + * 3. Parses positional arguments based on the command definition. + * 4. Converts arguments to their specified types. + * 5. Validates required arguments. + */ +export class CommandParser { + private registry: CommandRegistry; + + /** + * Creates an instance of CommandParser. + * + * @param registry - The command registry to use. + */ + constructor(registry: CommandRegistry) { + this.registry = registry; + } + + /** + * Parses the input string into a ParsedCommand object. + * + * @param input - The input string to parse. + * @returns The parsed command and its arguments. + * @throws If no command is provided or the command is unknown. + */ + parse(input: string): ParsedCommand { + const tokens = this.tokenize(input); + if (tokens.length === 0) { + throw new Error('No command provided.'); + } + + const commandName = tokens.shift()!; + const command = this.registry.get(commandName); + if (!command) { + throw new Error(`Unknown command: "${commandName}". Use "hyprpanel explain" for available commands.`); + } + + const args = this.parseArgs(command, tokens); + return { command, args }; + } + + /** + * Tokenizes the input string into an array of tokens. + * + * @param input - The input string to tokenize. + * @returns The array of tokens. + */ + private tokenize(input: string): string[] { + const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g; + const matches = input.match(regex); + return matches ? matches.map((token) => this.stripQuotes(token)) : []; + } + + /** + * Strips quotes from the beginning and end of a string. + * + * @param str - The string to strip quotes from. + * @returns The string without quotes. + */ + private stripQuotes(str: string): string { + return str.replace(/^["'](.+(?=["']$))["']$/, '$1'); + } + + /** + * Parses the positional arguments for a command. + * + * @param command - The command definition. + * @param tokens - The array of argument tokens. + * @returns The parsed arguments. + * @throws If there are too many arguments or a required argument is missing. + */ + private parseArgs(command: Command, tokens: string[]): Record { + const args: Record = {}; + const argDefs = command.args; + + if (tokens.length > argDefs.length) { + throw new Error(`Too many arguments for command "${command.name}". Expected at most ${argDefs.length}.`); + } + + argDefs.forEach((argDef, index) => { + const value = tokens[index]; + if (value === undefined) { + if (argDef.required) { + throw new Error(`Missing required argument: "${argDef.name}".`); + } + if (argDef.default !== undefined) { + args[argDef.name] = argDef.default; + } + return; + } + + args[argDef.name] = this.convertType(value, argDef.type); + }); + + return args; + } + + /** + * Converts a string value to the specified type. + * + * @param value - The value to convert. + * @param type - The type to convert to. + * @returns The converted value. + * @throws If the value cannot be converted to the specified type. + */ + private convertType( + value: string, + type: 'string' | 'number' | 'boolean' | 'object', + ): string | number | boolean | Record { + switch (type) { + case 'number': + const num = Number(value); + if (isNaN(num)) { + throw new Error(`Expected a number but got "${value}".`); + } + return num; + case 'boolean': + if (value.toLowerCase() === 'true') return true; + if (value.toLowerCase() === 'false') return false; + throw new Error(`Expected a boolean (true/false) but got "${value}".`); + case 'object': + try { + return JSON.parse(value); + } catch { + throw new Error(`Invalid JSON object: "${value}".`); + } + case 'string': + default: + return value; + } + } +} diff --git a/src/cli/commander/Registry.ts b/src/cli/commander/Registry.ts new file mode 100644 index 000000000..be872b1c4 --- /dev/null +++ b/src/cli/commander/Registry.ts @@ -0,0 +1,53 @@ +import { Command } from './types'; + +/** + * The CommandRegistry manages the storage and retrieval of commands. + * It supports registration of multiple commands, lookup by name or alias, + * and retrieval of all commands for listing and help functionalities. + */ +export class CommandRegistry { + private commands: Map = new Map(); + + /** + * Registers a command. If a command with the same name or alias already exists, + * it will throw an error. + * + * @param command - The command to register. + * @throws If a command with the same name or alias already exists. + */ + register(command: Command): void { + if (this.commands.has(command.name)) { + throw new Error(`Command "${command.name}" is already registered.`); + } + this.commands.set(command.name, command); + + if (command.aliases) { + for (const alias of command.aliases) { + if (this.commands.has(alias)) { + throw new Error(`Alias "${alias}" is already in use.`); + } + this.commands.set(alias, command); + } + } + } + + /** + * Retrieves a command by its name or alias. Returns undefined if not found. + * + * @param commandName - The name or alias of the command to retrieve. + * @returns The command if found, otherwise undefined. + */ + get(commandName: string): Command | undefined { + return this.commands.get(commandName); + } + + /** + * Retrieves all registered commands, ensuring each command is returned once even if it has aliases. + * + * @returns An array of all registered commands. + */ + getAll(): Command[] { + const unique = new Set(this.commands.values()); + return Array.from(unique); + } +} diff --git a/src/cli/commander/RequestHandler.ts b/src/cli/commander/RequestHandler.ts new file mode 100644 index 000000000..ea0cad3e3 --- /dev/null +++ b/src/cli/commander/RequestHandler.ts @@ -0,0 +1,89 @@ +import { CommandParser } from './Parser'; +import { ResponseCallback } from './types'; + +/** + * The RequestHandler orchestrates the parsing and execution of commands: + * 1. Uses the CommandParser to parse the input into a command and args. + * 2. Invokes the command handler with the parsed arguments. + * 3. Handles any errors and passes the result back via the response callback. + */ +export class RequestHandler { + private parser: CommandParser; + + /** + * Creates an instance of RequestHandler. + * + * @param parser - The CommandParser instance to use. + */ + constructor(parser: CommandParser) { + this.parser = parser; + } + + /** + * Initializes the request handler with the given input and response callback. + * + * @param input - The input string to process. + * @param response - The callback to handle the response. + * @returns A promise that resolves when the request is handled. + */ + async initializeRequestHandler(input: string, response: ResponseCallback): Promise { + try { + const parsed = this.parser.parse(input); + const { command, args } = parsed; + + const result = command.handler(args); + if (result instanceof Promise) { + const resolved = await result; + response(this.formatOutput(resolved)); + } else { + response(this.formatOutput(result)); + } + } catch (error) { + response(this.formatError(error)); + } + } + + /** + * Formats the output based on its type. + * + * @param output - The output to format. + * @returns A string representation of the output. + */ + private formatOutput(output: unknown): string { + if (typeof output === 'string') { + return output; + } else if (typeof output === 'number' || typeof output === 'boolean') { + return output.toString(); + } else if (typeof output === 'object' && output !== null) { + try { + return JSON.stringify(output, null, 2); + } catch { + return 'Unable to display object.'; + } + } else { + return String(output); + } + } + + /** + * Formats the error based on its type. + * + * @param error - The error to format. + * @returns A string representation of the error. + */ + private formatError(error: unknown): string { + if (error instanceof Error) { + return `Error: ${error.message}`; + } else if (typeof error === 'string') { + return `Error: ${error}`; + } else if (typeof error === 'object' && error !== null) { + try { + return `Error: ${JSON.stringify(error, null, 2)}`; + } catch { + return 'An unknown error occurred.'; + } + } else { + return `Error: ${String(error)}`; + } + } +} diff --git a/src/cli/commander/commands/appearance/index.ts b/src/cli/commander/commands/appearance/index.ts new file mode 100644 index 000000000..f4f78c6f2 --- /dev/null +++ b/src/cli/commander/commands/appearance/index.ts @@ -0,0 +1,75 @@ +import { errorHandler } from 'src/lib/utils'; +import { Command } from '../types'; +import { BarLayouts } from 'src/lib/types/options'; + +export const appearanceCommands: Command[] = [ + { + name: 'setWallpaper', + aliases: ['sw'], + description: 'Sets the wallpaper based on the provided input.', + category: 'Appearance', + args: [ + { + name: 'path', + description: 'Path to the wallpaper image.', + type: 'string', + required: true, + }, + ], + handler: (args: Record): string => { + try { + setWallpaper(args['path'] as string); + return 'Wallpaper set successfully.'; + } catch (error) { + if (error instanceof Error) { + return `Error setting wallpaper: ${error.message}`; + } + return `Error setting wallpaper: ${error}`; + } + }, + }, + { + name: 'useTheme', + aliases: ['ut'], + description: 'Sets the theme based on the provided input.', + category: 'Appearance', + args: [ + { + name: 'path', + description: 'Path to the JSON file of the HyprPanel theme.', + type: 'string', + required: true, + }, + ], + handler: (args: Record): string => { + try { + useTheme(args['path'] as string); + return 'Theme set successfully.'; + } catch (error) { + errorHandler(error); + } + }, + }, + { + name: 'setLayout', + aliases: ['slo'], + description: 'Sets the layout of the modules on the bar.', + category: 'Appearance', + args: [ + { + name: 'layout', + description: 'Bar layout to apply. Wiki: https://hyprpanel.com/configuration/panel.html#layouts', + type: 'object', + required: true, + }, + ], + handler: (args: Record): string => { + try { + setLayout(args['layout'] as BarLayouts); + return 'Layout applied successfully.'; + } catch (error) { + errorHandler(error); + } + }, + }, +]; diff --git a/src/cli/commander/commands/utility/checkDependencies.ts b/src/cli/commander/commands/utility/checkDependencies.ts new file mode 100644 index 000000000..70dab01ae --- /dev/null +++ b/src/cli/commander/commands/utility/checkDependencies.ts @@ -0,0 +1,367 @@ +import { GLib } from 'astal'; +import { errorHandler } from 'src/lib/utils'; + +const RED = '\x1b[31m'; +const GREEN = '\x1b[32m'; +const YELLOW = '\x1b[33m'; +const RESET = '\x1b[0m'; +const BOLD = '\x1b[1m'; + +const STATUS_INSTALLED = '(INSTALLED)'; +const STATUS_ACTIVE = '(ACTIVE)'; +const STATUS_DISABLED = '(DISABLED)'; +const STATUS_MISSING = '(MISSING)'; + +/** + * Decodes a Uint8Array output into a trimmed UTF-8 string. + * + * @description Converts a Uint8Array output from a command execution into a human-readable string. + * + * @param output - The Uint8Array output to decode. + */ +function decodeOutput(output: Uint8Array): string { + const decoder = new TextDecoder(); + return decoder.decode(output).trim(); +} + +/** + * Spawns a command line synchronously and returns the exit code and output. + * + * @description Executes a shell command using GLib.spawn_command_line_sync and extracts the exit code, stdout, and stderr. + * + * @param command - The command to execute. + */ +function runCommand(command: string): CommandResult { + const [, out, err, exitCode] = GLib.spawn_command_line_sync(command); + const stdout = out ? decodeOutput(out) : ''; + const stderr = err ? decodeOutput(err) : ''; + return { + exitCode, + stdout, + stderr, + }; +} + +/** + * Colors a given text using ANSI color codes. + * + * @description Wraps the provided text with ANSI color codes. + * + * @param text - The text to color. + * @param color - The ANSI color code to use. + */ +function colorText(text: string, color: string): string { + return `${color}${text}${RESET}`; +} + +/** + * Checks if any of the given executables is installed by using `which`. + * + * @description Iterates through a list of executables and returns true if any are found. + * + * @param executables - The list of executables to check. + */ +function checkExecutable(executables: string[]): boolean { + for (const exe of executables) { + const { exitCode } = runCommand(`which ${exe}`); + + if (exitCode === 0) { + return true; + } + } + return false; +} + +/** + * Checks if any of the given libraries is installed using `pkg-config`. + * + * @description Uses `pkg-config --exists ` to determine if a library is installed. + * + * @param libraries - The list of libraries to check. + */ +function checkLibrary(libraries: string[]): boolean { + for (const lib of libraries) { + const { exitCode, stdout } = runCommand(`sh -c "ldconfig -p | grep ${lib}"`); + + if (exitCode === 0 && stdout.length > 0) { + return true; + } + } + return false; +} + +/** + * Checks the status of a service. + * + * @description Determines if a service is ACTIVE, INSTALLED (but not running), DISABLED, or MISSING. + * + * @param services - The list of services to check. + */ +function checkServiceStatus(services: string[]): ServiceStatus { + for (const svc of services) { + const activeResult = runCommand(`systemctl is-active ${svc}`); + const activeStatus = activeResult.stdout; + + if (activeStatus === 'active') { + return 'ACTIVE'; + } + + if (activeStatus === 'inactive' || activeStatus === 'failed') { + const enabledResult = runCommand(`systemctl is-enabled ${svc}`); + const enabledStatus = enabledResult.stdout; + + if (enabledResult && (enabledStatus === 'enabled' || enabledStatus === 'static')) { + return 'INSTALLED'; + } else if (enabledResult && enabledStatus === 'disabled') { + return 'DISABLED'; + } else { + return 'MISSING'; + } + } + + if (activeStatus === 'unknown' || activeResult.exitCode !== 0) { + continue; + } + } + + return 'MISSING'; +} + +/** + * Determines the status string and color for a dependency based on its type and checks. + * + * @description Returns the formatted line indicating the status of the given dependency. + * + * @param dep - The dependency to check. + */ +function getDependencyStatus(dep: Dependency): string { + let status: ServiceStatus | 'INSTALLED' | 'MISSING'; + + switch (dep.type) { + case 'executable': + status = checkExecutable(dep.check) ? 'INSTALLED' : 'MISSING'; + break; + case 'library': + status = checkLibrary(dep.check) ? 'INSTALLED' : 'MISSING'; + break; + case 'service': + status = checkServiceStatus(dep.check); + break; + default: + status = 'MISSING'; + } + + let color: string; + let textStatus: string; + + switch (status) { + case 'ACTIVE': + textStatus = STATUS_ACTIVE; + color = GREEN; + break; + case 'INSTALLED': + textStatus = STATUS_INSTALLED; + color = GREEN; + break; + case 'DISABLED': + textStatus = STATUS_DISABLED; + color = YELLOW; + break; + case 'MISSING': + default: + textStatus = STATUS_MISSING; + color = RED; + break; + } + + if (!dep.description) { + return ` ${colorText(textStatus, color)} ${dep.package}`; + } + + return ` ${colorText(textStatus, color)} ${dep.package}: ${dep.description ?? ''}`; +} + +/** + * Checks all dependencies and returns a formatted output. + * + * @description Gathers the status of both required and optional dependencies and formats the result. + */ +export function checkDependencies(): string { + try { + const dependencies: Dependency[] = [ + { + package: 'wireplumber', + required: true, + type: 'executable', + check: ['wireplumber'], + }, + { + package: 'libgtop', + required: true, + type: 'library', + check: ['gtop-2.0'], + }, + { + package: 'bluez', + required: true, + type: 'service', + check: ['bluetooth.service'], + }, + { + package: 'bluez-utils', + required: true, + type: 'executable', + check: ['bluetoothctl'], + }, + { + package: 'networkmanager', + required: true, + type: 'service', + check: ['NetworkManager.service'], + }, + { + package: 'dart-sass', + required: true, + type: 'executable', + check: ['sass'], + }, + { + package: 'wl-clipboard', + required: true, + type: 'executable', + check: ['wl-copy', 'wl-paste'], + }, + { + package: 'upower', + required: true, + type: 'service', + check: ['upower.service'], + }, + { + package: 'aylurs-gtk-shell', + required: true, + type: 'executable', + check: ['ags'], + }, + + { + package: 'python', + required: false, + type: 'executable', + check: ['python', 'python3'], + description: 'GPU usage tracking (NVidia only)', + }, + { + package: 'python-gpustat', + required: false, + type: 'executable', + check: ['gpustat'], + description: 'GPU usage tracking (NVidia only)', + }, + { + package: 'pywal', + required: false, + type: 'executable', + check: ['wal'], + description: 'Pywal hook for wallpapers', + }, + { + package: 'pacman-contrib', + required: false, + type: 'executable', + check: ['paccache', 'rankmirrors'], + description: 'Checking for pacman updates', + }, + { + package: 'power-profiles-daemon', + required: false, + type: 'service', + check: ['power-profiles-daemon.service'], + description: 'Switch power profiles', + }, + { + package: 'swww', + required: false, + type: 'executable', + check: ['swww'], + description: 'Setting wallpapers', + }, + { + package: 'grimblast', + required: false, + type: 'executable', + check: ['grimblast'], + description: 'For the snapshot shortcut', + }, + { + package: 'brightnessctl', + required: false, + type: 'executable', + check: ['brightnessctl'], + description: 'To control keyboard and screen brightness', + }, + { + package: 'btop', + required: false, + type: 'executable', + check: ['btop'], + description: 'To view system resource usage', + }, + { + package: 'gpu-screen-recorder', + required: false, + type: 'executable', + check: ['gpu-screen-recorder'], + description: 'To use the built-in screen recorder', + }, + { + package: 'hyprpicker', + required: false, + type: 'executable', + check: ['hyprpicker'], + description: 'To use the preset color picker shortcut', + }, + { + package: 'matugen', + required: false, + type: 'executable', + check: ['matugen'], + description: 'To use wallpaper-based color schemes', + }, + ]; + + let output = `${BOLD}Required Dependencies:${RESET}\n`; + + for (const dep of dependencies.filter((d) => d.required)) { + output += getDependencyStatus(dep) + '\n'; + } + + output += `\n${BOLD}Optional Dependencies:${RESET}\n`; + + for (const dep of dependencies.filter((d) => !d.required)) { + output += getDependencyStatus(dep) + '\n'; + } + + return output; + } catch (error) { + errorHandler(error); + } +} + +type CommandResult = { + exitCode: number; + stdout: string; + stderr: string; +}; + +type DependencyType = 'executable' | 'library' | 'service'; + +type ServiceStatus = 'ACTIVE' | 'INSTALLED' | 'DISABLED' | 'MISSING'; + +type Dependency = { + package: string; + required: boolean; + type: DependencyType; + check: string[]; + description?: string; +}; diff --git a/src/cli/commander/commands/utility/index.ts b/src/cli/commander/commands/utility/index.ts new file mode 100644 index 000000000..353a8d5e6 --- /dev/null +++ b/src/cli/commander/commands/utility/index.ts @@ -0,0 +1,104 @@ +import { errorHandler } from 'src/lib/utils'; +import { Command } from '../../types'; +import { execAsync, Gio, GLib } from 'astal'; +import { checkDependencies } from './checkDependencies'; + +export const utilityCommands: Command[] = [ + { + name: 'systrayItems', + aliases: ['sti'], + description: 'Gets a list of IDs for the current applications in the system tray.', + category: 'Utility', + args: [], + handler: (): string => { + try { + return getSystrayItems(); + } catch (error) { + errorHandler(error); + } + }, + }, + { + name: 'clearNotifications', + aliases: ['cno'], + description: 'Clears all of the notifications that currently exist.', + category: 'Utility', + args: [], + handler: (): string => { + try { + clearAllNotifications(); + return 'Notifications cleared successfully.'; + } catch (error) { + errorHandler(error); + } + }, + }, + { + name: 'migrateConfig', + aliases: ['mcfg'], + description: 'Migrates the configuration file from the old location to the new one.', + category: 'Utility', + args: [], + handler: (): string => { + const oldPath = `${GLib.get_user_cache_dir()}/ags/hyprpanel/options.json`; + + try { + const oldFile = Gio.File.new_for_path(oldPath); + const newFile = Gio.File.new_for_path(CONFIG); + + if (oldFile.query_exists(null)) { + oldFile.move(newFile, Gio.FileCopyFlags.OVERWRITE, null, null); + return `Configuration file moved to ${CONFIG}`; + } else { + return `Old configuration file does not exist at ${oldPath}`; + } + } catch (error) { + errorHandler(error); + } + }, + }, + { + name: 'checkDependencies', + aliases: ['chd'], + description: 'Checks the status of required and optional dependencies.', + category: 'Utility', + args: [], + handler: (): string => { + try { + return checkDependencies(); + } catch (error) { + errorHandler(error); + } + }, + }, + { + name: 'restart', + aliases: ['r'], + description: 'Restarts HyprPanel.', + category: 'Utility', + args: [], + handler: (): string => { + try { + execAsync('bash -c "hyprpanel -q; hyprpanel"'); + return ''; + } catch (error) { + errorHandler(error); + } + }, + }, + { + name: 'quit', + aliases: ['q'], + description: 'Quits HyprPanel.', + category: 'Utility', + args: [], + handler: (): string => { + try { + execAsync('bash -c "hyprpanel -q"'); + return ''; + } catch (error) { + errorHandler(error); + } + }, + }, +]; diff --git a/src/cli/commander/commands/windowManagement/index.ts b/src/cli/commander/commands/windowManagement/index.ts new file mode 100644 index 000000000..ae43d79aa --- /dev/null +++ b/src/cli/commander/commands/windowManagement/index.ts @@ -0,0 +1,70 @@ +import { errorHandler } from 'src/lib/utils'; +import { Command } from '../types'; +import { App } from 'astal/gtk3'; + +export const windowManagementCommands: Command[] = [ + { + name: 'isWindowVisible', + aliases: ['iwv'], + description: 'Checks if a specified window is visible.', + category: 'Window Management', + args: [ + { + name: 'window', + description: 'Name of the window to check.', + type: 'string', + required: true, + }, + ], + handler: (args: Record): boolean => { + return isWindowVisible(args['window'] as string); + }, + }, + { + name: 'toggleWindow', + aliases: ['t'], + description: 'Toggles the visibility of a specified window.', + category: 'Window Management', + args: [ + { + name: 'window', + description: 'The name of the window to toggle.', + type: 'string', + required: true, + }, + ], + handler: (args: Record): string => { + try { + const windowName = args['window'] as string; + const foundWindow = App.get_window(windowName); + + if (!foundWindow) { + throw new Error(`Window ${args['window']} not found.`); + } + + const windowStatus = foundWindow.visible ? 'hidden' : 'visible'; + + App.toggle_window(windowName); + + return windowStatus; + } catch (error) { + errorHandler(error); + } + }, + }, + { + name: 'listWindows', + aliases: ['lw'], + description: 'Gets a list of all HyprPanel windows.', + category: 'Window Management', + args: [], + handler: (): string => { + try { + const windowList = App.get_windows().map((window) => window.name); + return windowList.join('\n'); + } catch (error) { + errorHandler(error); + } + }, + }, +]; diff --git a/src/cli/commander/helpers/index.ts b/src/cli/commander/helpers/index.ts new file mode 100644 index 000000000..6d6a141f3 --- /dev/null +++ b/src/cli/commander/helpers/index.ts @@ -0,0 +1,195 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { CommandRegistry } from '../Registry'; +import { CategoryMap, Command, PositionalArg } from '../types'; + +const ANSI_RESET = '\x1b[0m'; +const ANSI_BOLD = '\x1b[1m'; +const ANSI_UNDERLINE = '\x1b[4m'; + +// Foreground Colors +const ANSI_FG_RED = '\x1b[31m'; +const ANSI_FG_GREEN = '\x1b[32m'; +const ANSI_FG_YELLOW = '\x1b[33m'; +const ANSI_FG_BLUE = '\x1b[34m'; +const ANSI_FG_MAGENTA = '\x1b[35m'; +const ANSI_FG_CYAN = '\x1b[36m'; +const ANSI_FG_WHITE = '\x1b[37m'; + +// Background Colors +const ANSI_BG_RED = '\x1b[41m'; +const ANSI_BG_GREEN = '\x1b[42m'; +const ANSI_BG_YELLOW = '\x1b[43m'; +const ANSI_BG_BLUE = '\x1b[44m'; +const ANSI_BG_MAGENTA = '\x1b[45m'; +const ANSI_BG_CYAN = '\x1b[46m'; +const ANSI_BG_WHITE = '\x1b[47m'; + +/** + * Creates the explain command. + * + * This command displays all available commands categorized by their respective + * categories. If a specific command name is provided as an argument, it displays + * detailed information about that command, including its positional parameters and aliases. + * + * @param registry - The command registry to use. + * @returns The explain command. + */ +export function createExplainCommand(registry: CommandRegistry): Command { + return { + name: 'explain', + aliases: ['e'], + description: 'Displays explain information for all commands or a specific command.', + category: 'General', + args: [ + { + name: 'commandName', + description: 'Optional name of a command to get detailed info.', + type: 'string', + required: false, + }, + ], + /** + * Handler for the explain command. + * + * @param args - The arguments passed to the command. + * @returns The formatted explain message. + */ + handler: (args: Record): string => { + const commandName = args['commandName'] as string | undefined; + + if (commandName) { + return formatCommandExplain(registry, commandName); + } + + return formatGlobalExplain(registry); + }, + }; +} + +/** + * Formats the detailed explain message for a specific command. + * + * @param registry - The command registry to retrieve the command. + * @param commandName - The name of the command to get detailed explain for. + * @returns The formatted detailed explain message. + */ +function formatCommandExplain(registry: CommandRegistry, commandName: string): string { + const cmd = registry.get(commandName); + if (!cmd) { + return `${ANSI_FG_RED}✖ No such command: "${commandName}". Use "explain" to see all commands.${ANSI_RESET}\n`; + } + + let message = `${ANSI_BOLD}${ANSI_FG_YELLOW}Command: ${cmd.name}${ANSI_RESET}\n`; + + if (cmd.aliases && cmd.aliases.length > 0) { + const aliases = formatAliases(cmd.aliases); + message += `${ANSI_FG_GREEN}Aliases:${ANSI_RESET} ${aliases}\n`; + } + + message += `${ANSI_FG_GREEN}Description:${ANSI_RESET} ${cmd.description}\n`; + message += `${ANSI_FG_GREEN}Category:${ANSI_RESET} ${cmd.category}\n`; + + if (cmd.args.length > 0) { + message += `${ANSI_FG_GREEN}Arguments:${ANSI_RESET}\n`; + const formattedArgs = formatArguments(cmd.args); + message += formattedArgs; + } else { + message += `${ANSI_FG_GREEN}No positional arguments.${ANSI_RESET}`; + } + + return message; +} + +/** + * Formats the global explain message listing all available commands categorized by their categories. + * + * @param registry - The command registry to retrieve all commands. + * @returns The formatted global explain message. + */ +function formatGlobalExplain(registry: CommandRegistry): string { + const allCommands = registry.getAll(); + const categoryMap: CategoryMap = organizeCommandsByCategory(allCommands); + + let explainMessage = `${ANSI_BOLD}${ANSI_FG_CYAN}Available HyprPanel Commands:${ANSI_RESET}\n`; + + for (const [category, cmds] of Object.entries(categoryMap)) { + explainMessage += `\n${ANSI_BOLD}${ANSI_FG_BLUE}${category}${ANSI_RESET}\n`; + const formattedCommands = formatCommandList(cmds); + explainMessage += formattedCommands; + } + + explainMessage += `\n${ANSI_FG_MAGENTA}Use "hyprpanel explain " to get detailed information about a specific hyprpanel command.${ANSI_RESET}\n`; + + return explainMessage.trim(); +} + +/** + * Organizes commands into their respective categories. + * + * @param commands - The list of all commands. + * @returns A mapping of category names to arrays of commands. + */ +function organizeCommandsByCategory(commands: Command[]): CategoryMap { + const categoryMap: CategoryMap = {}; + + commands.forEach((cmd) => { + if (!categoryMap[cmd.category]) { + categoryMap[cmd.category] = []; + } + categoryMap[cmd.category].push(cmd); + }); + + return categoryMap; +} + +/** + * Formats the list of commands under a specific category. + * + * @param commands - The list of commands in a category. + * @returns A formatted string of commands. + */ +function formatCommandList(commands: Command[]): string { + return ( + commands + .map((cmd) => { + const aliasesText = + cmd.aliases && cmd.aliases.length > 0 + ? ` (${cmd.aliases.map((alias) => `${ANSI_FG_CYAN}${alias}${ANSI_RESET}`).join(', ')})` + : ''; + return ` - ${ANSI_FG_YELLOW}${cmd.name}${ANSI_RESET}${aliasesText}: ${cmd.description}`; + }) + .join('\n') + '\n' + ); +} + +/** + * Formats the aliases array into a readable string with appropriate coloring. + * + * @param aliases - The array of alias strings. + * @returns The formatted aliases string. + */ +function formatAliases(aliases: string[]): string { + return aliases.map((alias) => `${ANSI_FG_CYAN}${alias}${ANSI_RESET}`).join(', '); +} + +/** + * Formats the arguments array into a readable string with appropriate coloring. + * + * @param args - The array of positional arguments. + * @returns The formatted arguments string. + */ +function formatArguments(args: PositionalArg[]): string { + return ( + args + .map((arg) => { + const requirement = arg.required ? `${ANSI_FG_RED}(required)` : `${ANSI_FG_CYAN}(optional)`; + const defaultValue = + arg.default !== undefined + ? ` ${ANSI_FG_MAGENTA}[default: ${JSON.stringify(arg.default)}]${ANSI_RESET}` + : ''; + return ` ${ANSI_FG_YELLOW}${arg.name}${ANSI_RESET}: ${arg.description} ${requirement}${defaultValue}`; + }) + .join('\n') + '\n' + ); +} diff --git a/src/cli/commander/index.ts b/src/cli/commander/index.ts new file mode 100644 index 000000000..fa1cce218 --- /dev/null +++ b/src/cli/commander/index.ts @@ -0,0 +1,35 @@ +import { CommandRegistry } from './Registry'; +import { CommandParser } from './Parser'; +import { RequestHandler } from './RequestHandler'; +import { initializeCommands } from './InitializeCommand'; +import { ResponseCallback } from './types'; + +/** + * This is the entry point for the CLI. It: + * 1. Creates a CommandRegistry + * 2. Initializes all commands + * 3. Creates a CommandParser + * 4. Creates a RequestHandler + * 5. Provides a function `runCLI` to process an input string and respond with a callback. + */ + +const registry = new CommandRegistry(); + +initializeCommands(registry); + +const parser = new CommandParser(registry); +const handler = new RequestHandler(parser); + +/** + * Run the CLI with a given input and a response callback. + * + * @param input - The input string to process. + * @param response - The callback to handle the response. + */ +export function runCLI(input: string, response: ResponseCallback): void { + handler.initializeRequestHandler(input, response).catch((err) => { + response({ error: err instanceof Error ? err.message : String(err) }); + }); +} + +export { registry }; diff --git a/src/cli/commander/types.ts b/src/cli/commander/types.ts new file mode 100644 index 000000000..4b4105159 --- /dev/null +++ b/src/cli/commander/types.ts @@ -0,0 +1,31 @@ +export interface PositionalArg { + name: string; + description: string; + type: 'string' | 'number' | 'boolean' | 'object'; + required?: boolean; + default?: string | number | boolean | Record; +} + +export type HandlerReturn = unknown | Promise; + +export interface Command { + name: string; + aliases?: string[]; + description: string; + category: string; + args: PositionalArg[]; + handler: (args: Record) => HandlerReturn; +} + +export interface ParsedCommand { + command: Command; + args: Record; +} + +export interface ResponseCallback { + (res: unknown): void; +} + +export interface CategoryMap { + [category: string]: Command[]; +} diff --git a/src/components/bar/exports.ts b/src/components/bar/exports.ts new file mode 100644 index 000000000..13da91325 --- /dev/null +++ b/src/components/bar/exports.ts @@ -0,0 +1,53 @@ +import { Menu } from './modules/menu'; +import { Workspaces } from '../../components/bar/modules/workspaces/index'; +import { ClientTitle } from '../../components/bar/modules/window_title/index'; +import { Media } from '../../components/bar/modules/media/index'; +import { Notifications } from '../../components/bar/modules/notifications/index'; +import { Volume } from '../../components/bar/modules/volume/index'; +import { Network } from '../../components/bar/modules/network/index'; +import { Bluetooth } from '../../components/bar/modules/bluetooth/index'; +import { BatteryLabel } from '../../components/bar/modules/battery/index'; +import { Clock } from '../../components/bar/modules/clock/index'; +import { SysTray } from '../../components/bar/modules/systray/index'; + +// Custom Modules +import { Ram } from '../../components/bar/modules/ram/index'; +import { Cpu } from '../../components/bar/modules/cpu/index'; +import { CpuTemp } from '../../components/bar/modules/cputemp/index'; +import { Storage } from '../../components/bar/modules/storage/index'; +import { Netstat } from '../../components/bar/modules/netstat/index'; +import { KbInput } from '../../components/bar/modules/kblayout/index'; +import { Updates } from '../../components/bar/modules/updates/index'; +import { Submap } from '../../components/bar/modules/submap/index'; +import { Weather } from '../../components/bar/modules/weather/index'; +import { Power } from '../../components/bar/modules/power/index'; +import { Hyprsunset } from '../../components/bar/modules/hyprsunset/index'; +import { Hypridle } from '../../components/bar/modules/hypridle/index'; + +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, +}; diff --git a/src/components/bar/index.tsx b/src/components/bar/index.tsx new file mode 100644 index 000000000..9439727db --- /dev/null +++ b/src/components/bar/index.tsx @@ -0,0 +1,169 @@ +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 { WidgetContainer } from './shared/WidgetContainer'; +import options from 'src/options'; +import { App, Gtk } from 'astal/gtk3/index'; + +import Astal from 'gi://Astal?version=3.0'; +import { bind, Variable } from 'astal'; +import { gdkMonitorIdToHyprlandId, getLayoutForMonitor, isLayoutEmpty } from './utils/monitors'; + +const { layouts } = options.bar; +const { location } = options.theme.bar; +const { location: borderLocation } = options.theme.bar.border; + +const widget = { + battery: (): JSX.Element => WidgetContainer(BatteryLabel()), + dashboard: (): JSX.Element => WidgetContainer(Menu()), + workspaces: (monitor: number): JSX.Element => WidgetContainer(Workspaces(monitor)), + windowtitle: (): JSX.Element => WidgetContainer(ClientTitle()), + media: (): JSX.Element => WidgetContainer(Media()), + notifications: (): JSX.Element => WidgetContainer(Notifications()), + volume: (): JSX.Element => WidgetContainer(Volume()), + network: (): JSX.Element => WidgetContainer(Network()), + bluetooth: (): JSX.Element => WidgetContainer(Bluetooth()), + clock: (): JSX.Element => WidgetContainer(Clock()), + systray: (): JSX.Element => WidgetContainer(SysTray()), + ram: (): JSX.Element => WidgetContainer(Ram()), + cpu: (): JSX.Element => WidgetContainer(Cpu()), + cputemp: (): JSX.Element => WidgetContainer(CpuTemp()), + storage: (): JSX.Element => WidgetContainer(Storage()), + netstat: (): JSX.Element => WidgetContainer(Netstat()), + kbinput: (): JSX.Element => WidgetContainer(KbInput()), + updates: (): JSX.Element => WidgetContainer(Updates()), + submap: (): JSX.Element => WidgetContainer(Submap()), + weather: (): JSX.Element => WidgetContainer(Weather()), + power: (): JSX.Element => WidgetContainer(Power()), + hyprsunset: (): JSX.Element => WidgetContainer(Hyprsunset()), + hypridle: (): JSX.Element => WidgetContainer(Hypridle()), +}; + +export const Bar = (() => { + const usedHyprlandMonitors = new Set(); + + return (monitor: number): JSX.Element => { + const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors); + + const computeVisibility = bind(layouts).as(() => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.get()); + return !isLayoutEmpty(foundLayout); + }); + + const computeAnchor = bind(location).as((loc) => { + if (loc === 'bottom') { + return Astal.WindowAnchor.BOTTOM | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; + } + + return Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT | Astal.WindowAnchor.RIGHT; + }); + + const computeLayer = Variable.derive([bind(options.theme.bar.layer), bind(options.tear)], (barLayer, tear) => { + if (tear && barLayer === 'overlay') { + return Astal.Layer.TOP; + } + const layerMap = { + overlay: Astal.Layer.OVERLAY, + top: Astal.Layer.TOP, + bottom: Astal.Layer.BOTTOM, + background: Astal.Layer.BACKGROUND, + }; + + return layerMap[barLayer]; + }); + + const computeBorderLocation = bind(borderLocation).as((brdrLcn) => + brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel', + ); + + const leftBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + + return foundLayout.left + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + const middleBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + + return foundLayout.middle + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + const rightBinding = Variable.derive([bind(layouts)], (currentLayouts) => { + const foundLayout = getLayoutForMonitor(hyprlandMonitor, currentLayouts); + + return foundLayout.right + .filter((mod) => Object.keys(widget).includes(mod)) + .map((w) => widget[w](hyprlandMonitor)); + }); + + return ( + { + computeLayer.drop(); + leftBinding.drop(); + middleBinding.drop(); + rightBinding.drop(); + }} + > + + + {leftBinding()} + + } + centerWidget={ + + {middleBinding()} + + } + endWidget={ + + {rightBinding()} + + } + /> + + + ); + }; +})(); diff --git a/src/components/bar/modules/battery/helpers/index.ts b/src/components/bar/modules/battery/helpers/index.ts new file mode 100644 index 000000000..16434936b --- /dev/null +++ b/src/components/bar/modules/battery/helpers/index.ts @@ -0,0 +1,51 @@ +import { BatteryIconKeys, BatteryIcons } from 'src/lib/types/battery'; + +const batteryIcons: BatteryIcons = { + 0: '󰂎', + 10: '󰁺', + 20: '󰁻', + 30: '󰁼', + 40: '󰁽', + 50: '󰁾', + 60: '󰁿', + 70: '󰂀', + 80: '󰂁', + 90: '󰂂', + 100: '󰁹', +}; + +const batteryIconsCharging: BatteryIcons = { + 0: '󰢟', + 10: '󰢜', + 20: '󰂆', + 30: '󰂇', + 40: '󰂈', + 50: '󰢝', + 60: '󰂉', + 70: '󰢞', + 80: '󰂊', + 90: '󰂋', + 100: '󰂅', +}; + +/** + * Retrieves the appropriate battery icon based on the battery percentage and charging status. + * + * This function returns the corresponding battery icon based on the provided battery percentage, charging status, and whether the battery is fully charged. + * It uses predefined mappings for battery icons and charging battery icons. + * + * @param percentage The current battery percentage. + * @param charging A boolean indicating whether the battery is currently charging. + * @param isCharged A boolean indicating whether the battery is fully charged. + * + * @returns The corresponding battery icon as a string. + */ +export const getBatteryIcon = (percentage: number, charging: boolean, isCharged: boolean): string => { + if (isCharged) { + return '󱟢'; + } + const percentages: BatteryIconKeys[] = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0]; + const foundPercentage = percentages.find((threshold) => threshold <= percentage) ?? 100; + + return charging ? batteryIconsCharging[foundPercentage] : batteryIcons[foundPercentage]; +}; diff --git a/src/components/bar/modules/battery/index.tsx b/src/components/bar/modules/battery/index.tsx new file mode 100644 index 000000000..7382d58ea --- /dev/null +++ b/src/components/bar/modules/battery/index.tsx @@ -0,0 +1,133 @@ +import { batteryService } from 'src/lib/constants/services.js'; +import { Astal } from 'astal/gtk3'; +import { openMenu } from '../../utils/menu'; +import options from 'src/options'; +import { BarBoxChild } from 'src/lib/types/bar.js'; +import { runAsyncCommand, throttledScrollHandler } from 'src/components/bar/utils/helpers.js'; +import Variable from 'astal/variable'; +import { bind } from 'astal/binding.js'; +import AstalBattery from 'gi://AstalBattery?version=0.1'; +import { useHook } from 'src/lib/shared/hookHandler'; +import { onMiddleClick, onPrimaryClick, onScroll, onSecondaryClick } from 'src/lib/shared/eventHandlers'; +import { getBatteryIcon } from './helpers'; + +const { label: show_label, rightClick, middleClick, scrollUp, scrollDown, hideLabelWhenFull } = options.bar.battery; + +const BatteryLabel = (): BarBoxChild => { + const batIcon = Variable.derive( + [bind(batteryService, 'percentage'), bind(batteryService, 'charging'), bind(batteryService, 'state')], + (batPercent: number, batCharging: boolean, state: AstalBattery.State) => { + const batCharged = state === AstalBattery.State.FULLY_CHARGED; + + return getBatteryIcon(Math.floor(batPercent * 100), batCharging, batCharged); + }, + ); + + const formatTime = (seconds: number): Record => { + 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 === true) { + 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`; + } + }; + + const componentClassName = Variable.derive( + [bind(options.theme.bar.buttons.style), bind(show_label)], + (style, showLabel) => { + const styleMap = { + default: 'style1', + split: 'style2', + wave: 'style3', + wave2: 'style3', + }; + return `battery-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`; + }, + ); + + const componentTooltip = Variable.derive( + [bind(batteryService, 'charging'), bind(batteryService, 'timeToFull'), bind(batteryService, 'timeToEmpty')], + (isCharging, timeToFull, timeToEmpty) => { + const timeRemaining = isCharging ? timeToFull : timeToEmpty; + return generateTooltip(timeRemaining, isCharging, Math.floor(batteryService.percentage * 100) === 100); + }, + ); + + const componentChildren = Variable.derive( + [bind(show_label), bind(batteryService, 'percentage'), bind(hideLabelWhenFull)], + (showLabel, percentage, hideLabelWhenFull) => { + const isCharged = Math.round(percentage) === 100; + + const icon =