Upgrade to Agsv2 + Astal (#533)
* migrate to astal * Reorganize project structure. * progress * Migrate Dashboard and Window Title modules. * Migrate clock and notification bar modules. * Remove unused code * Media menu * Rework network and volume modules * Finish custom modules. * Migrate battery bar module. * Update battery module and organize helpers. * Migrate workspace module. * Wrap up bar modules. * Checkpoint before I inevitbly blow something up. * Updates * Fix event propagation logic. * Type fixes * More type fixes * Fix padding for event boxes. * Migrate volume menu and refactor scroll event handlers. * network module WIP * Migrate network service. * Migrate bluetooth menu * Updates * Migrate notifications * Update scrolling behavior for custom modules. * Improve popup notifications and add timer functionality. * Migration notifications menu header/controls. * Migrate notifications menu and consolidate notifications menu code. * Migrate power menu. * Dashboard progress * Migrate dashboard * Migrate media menu. * Reduce media menu nesting. * Finish updating media menu bindings to navigate active player. * Migrate battery menu * Consolidate code * Migrate calendar menu * Fix workspace logic to update on client add/change/remove and consolidate code. * Migrate osd * Consolidate hyprland service connections. * Implement startup dropdown menu position allocation. * Migrate settings menu (WIP) * Settings dialo menu fixes * Finish Dashboard menu * Type updates * update submoldule for types * update github ci * ci * Submodule update * Ci updates * Remove type checking for now. * ci fix * Fix a bunch of stuff, losing track... need rest. Brb coffee * Validate dropdown menu before render. * Consolidate code and add auto-hide functionality. * Improve auto-hide behavior. * Consolidate audio menu code * Organize bluetooth code * Improve active player logic * Properly dismiss a notification on action button resolution. * Implement CLI command engine and migrate CLI commands. * Handle variable disposal * Bar component fixes and add hyprland startup rules. * Handle potentially null bindings network and bluetooth bindings. * Handle potentially null wired adapter. * Fix GPU stats * Handle poller for GPU * Fix gpu bar logic. * Clean up logic for stat bars. * Handle wifi and wired bar icon bindings. * Fix battery percentages * Fix switch behavior * Wifi staging fixes * Reduce redundant hyprland service calls. * Code cleanup * Document the option code and reduce redundant calls to optimize performance. * Remove outdated comment. * Add JSDocs * Add meson to build hyprpanel * Consistency updates * Organize commands * Fix images not showing up on notifications. * Remove todo * Move hyprpanel configuration to the ~/.config/hyprpanel directory and add utility commands. * Handle SRC directory for the bundled/built hyprpanel. * Add namespaces to all windows * Migrate systray * systray updates * Update meson to include ts, tsx and scss files. * Remove log from meson * Fix file choose path and make it float. * Added a command to check the dependency status * Update dep names. * Get scale directly from env * Add todo
This commit is contained in:
@@ -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,
|
||||
|
||||
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -10,21 +10,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout main repository
|
||||
- name: Checkout main repository with submodules
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Clone ags-types to temp dir
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: Jas-SinghFSU/ags-types
|
||||
path: temp-ags-types
|
||||
|
||||
- name: Copy types to types/
|
||||
run: |
|
||||
rm -rf types
|
||||
mkdir -p types
|
||||
cp -R temp-ags-types/types/* types/
|
||||
rm -rf temp-ags-types
|
||||
# with:
|
||||
# submodules: true
|
||||
#
|
||||
# - name: Clone astal repository to /usr/share/astal/gjs
|
||||
# run: |
|
||||
# sudo mkdir -p /usr/share/astal/
|
||||
# sudo git clone https://github.com/Jas-SinghFSU/astalgjs.git /usr/share/astal
|
||||
#
|
||||
# - name: Copy types to @girs/
|
||||
# run: |
|
||||
# rm -rf @girs
|
||||
# mkdir -p @girs
|
||||
# cp -R external/ags-types/@girs/* @girs/
|
||||
# rm -rf external/ags-types
|
||||
|
||||
- name: Node Setup
|
||||
uses: actions/setup-node@v3
|
||||
@@ -37,5 +38,5 @@ jobs:
|
||||
- name: ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Type Check
|
||||
run: npx tsc --noEmit --pretty --extendedDiagnostics
|
||||
# - name: Type Check
|
||||
# run: npx tsc --noEmit --pretty --extendedDiagnostics
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,4 @@
|
||||
.weather.json
|
||||
node_modules
|
||||
|
||||
@girs
|
||||
|
||||
5
.gitmodules
vendored
5
.gitmodules
vendored
@@ -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
|
||||
|
||||
|
||||
49
PKGBUILD
49
PKGBUILD
@@ -1,49 +0,0 @@
|
||||
# Big thanks to kotontrio for providing this. You can find it in his repo as well
|
||||
# as: https://github.com/kotontrion/PKGBUILDS/blob/main/agsv1/PKGBUILD
|
||||
#
|
||||
# Maintainer: kotontrion <kotontrion@tutanota.de>
|
||||
|
||||
# This package is only intended to be used while migrating from ags v1.8.2 to ags v2.0.0.
|
||||
# Many ags configs are quite big and it takes a while to migrate, therefore I made this package
|
||||
# to install ags v1.8.2 as "agsv1", so both versions can be installed at the same time, making it
|
||||
# possible to migrate bit by bit while still having a working v1 config around.
|
||||
#
|
||||
# First update the aylurs-gtk-shell package to v2, then install this one.
|
||||
#
|
||||
# This package won't receive any updates anymore, so as soon as you migrated, uninstall this one.
|
||||
|
||||
pkgname=agsv1
|
||||
_pkgname=ags
|
||||
pkgver=1.8.2
|
||||
pkgrel=1
|
||||
pkgdesc="Aylurs's Gtk Shell (AGS), An eww inspired gtk widget system."
|
||||
arch=('x86_64')
|
||||
url="https://github.com/Aylur/ags"
|
||||
license=('GPL-3.0-only')
|
||||
makedepends=('gobject-introspection' 'meson' 'glib2-devel' 'npm' 'typescript')
|
||||
depends=('gjs' 'glib2' 'glibc' 'gtk3' 'gtk-layer-shell' 'libpulse' 'pam')
|
||||
optdepends=('gnome-bluetooth-3.0: required for bluetooth service'
|
||||
'greetd: required for greetd service'
|
||||
'libdbusmenu-gtk3: required for systemtray service'
|
||||
'libsoup3: required for the Utils.fetch feature'
|
||||
'libnotify: required for sending notifications'
|
||||
'networkmanager: required for network service'
|
||||
'power-profiles-daemon: required for powerprofiles service'
|
||||
'upower: required for battery service')
|
||||
backup=('etc/pam.d/ags')
|
||||
source=("$pkgname-$pkgver.tar.gz::https://github.com/Aylur/ags/releases/download/v${pkgver}/ags-v${pkgver}.tar.gz")
|
||||
sha256sums=('ea0a706bef99578b30d40a2d0474b7a251364bfcf3a18cdc9b1adbc04af54773')
|
||||
|
||||
build() {
|
||||
cd $srcdir/$_pkgname
|
||||
npm install
|
||||
arch-meson build --libdir "lib/$_pkgname" -Dbuild_types=true
|
||||
meson compile -C build
|
||||
}
|
||||
|
||||
package() {
|
||||
cd $srcdir/$_pkgname
|
||||
meson install -C build --destdir "$pkgdir"
|
||||
rm ${pkgdir}/usr/bin/ags
|
||||
ln -sf /usr/share/com.github.Aylur.ags/com.github.Aylur.ags ${pkgdir}/usr/bin/agsv1
|
||||
}
|
||||
107
app.ts
Normal file
107
app.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import './src/lib/session';
|
||||
import './src/scss/style';
|
||||
import './src/globals/useTheme';
|
||||
import './src/globals/wallpaper';
|
||||
import './src/globals/systray';
|
||||
import './src/globals/dropdown';
|
||||
import './src/globals/utilities';
|
||||
import './src/components/bar/utils/sideEffects';
|
||||
|
||||
import { Bar } from './src/components/bar';
|
||||
import { DropdownMenus, StandardWindows } from './src/components/menus/exports';
|
||||
import Notifications from './src/components/notifications';
|
||||
import SettingsDialog from './src/components/settings/index';
|
||||
import { bash, forMonitors } from 'src/lib/utils';
|
||||
import options from 'src/options';
|
||||
import OSD from 'src/components/osd/index';
|
||||
import { App } from 'astal/gtk3';
|
||||
import { exec, execAsync } from 'astal';
|
||||
import { hyprlandService } from 'src/lib/constants/services';
|
||||
import { handleRealization } from 'src/components/menus/shared/dropdown/helpers';
|
||||
import { isDropdownMenu } from 'src/lib/constants/options.js';
|
||||
import { initializeSystemBehaviors } from 'src/lib/behaviors';
|
||||
import { runCLI } from 'src/cli/commander';
|
||||
|
||||
const initializeStartupScripts = (): void => {
|
||||
execAsync(`python3 ${SRC_DIR}/scripts/bluetooth.py`).catch((err) => console.error(err));
|
||||
};
|
||||
|
||||
const initializeMenus = (): void => {
|
||||
StandardWindows.forEach((window) => {
|
||||
return window();
|
||||
});
|
||||
|
||||
DropdownMenus.forEach((window) => {
|
||||
return window();
|
||||
});
|
||||
|
||||
DropdownMenus.forEach((window) => {
|
||||
const windowName = window.name.replace('_default', '').concat('menu').toLowerCase();
|
||||
|
||||
if (!isDropdownMenu(windowName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleRealization(windowName);
|
||||
});
|
||||
};
|
||||
|
||||
App.start({
|
||||
instanceName: 'hyprpanel',
|
||||
requestHandler(request: string, res: (response: unknown) => void) {
|
||||
runCLI(request, res);
|
||||
},
|
||||
main() {
|
||||
initializeStartupScripts();
|
||||
|
||||
Notifications();
|
||||
OSD();
|
||||
forMonitors(Bar).forEach((bar: JSX.Element) => bar);
|
||||
SettingsDialog();
|
||||
initializeMenus();
|
||||
|
||||
initializeSystemBehaviors();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Function to determine if the current OS is NixOS by parsing /etc/os-release.
|
||||
* @returns True if NixOS, false otherwise.
|
||||
*/
|
||||
const isNixOS = (): boolean => {
|
||||
try {
|
||||
const osRelease = exec('cat /etc/os-release').toString();
|
||||
const idMatch = osRelease.match(/^ID\s*=\s*"?([^"\n]+)"?/m);
|
||||
|
||||
if (idMatch && idMatch[1].toLowerCase() === 'nixos') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error detecting OS:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to generate the appropriate restart command based on the OS.
|
||||
* @returns The modified or original restart command.
|
||||
*/
|
||||
const getRestartCommand = (): string => {
|
||||
const isNix = isNixOS();
|
||||
const command = options.hyprpanel.restartCommand.get();
|
||||
|
||||
if (isNix) {
|
||||
return command.replace(/\bags\b/g, 'hyprpanel');
|
||||
}
|
||||
|
||||
return command;
|
||||
};
|
||||
|
||||
hyprlandService.connect('monitor-added', () => {
|
||||
if (options.hyprpanel.restartAgs.get()) {
|
||||
const restartAgsCommand = getRestartCommand();
|
||||
bash(restartAgsCommand);
|
||||
}
|
||||
});
|
||||
58
config.js
58
config.js
@@ -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 {};
|
||||
@@ -1,830 +0,0 @@
|
||||
import { Option } from 'widget/settings/shared/Option';
|
||||
import { Header } from 'widget/settings/shared/Header';
|
||||
|
||||
import options from 'options';
|
||||
import Scrollable from 'types/widgets/scrollable';
|
||||
import { Attribute, GtkWidget } from 'lib/types/widget';
|
||||
|
||||
export const CustomModuleSettings = (): Scrollable<GtkWidget, Attribute> =>
|
||||
Widget.Scrollable({
|
||||
vscroll: 'automatic',
|
||||
hscroll: 'automatic',
|
||||
class_name: 'menu-theme-page customModules paged-container',
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-theme-page paged-container',
|
||||
vertical: true,
|
||||
children: [
|
||||
/*
|
||||
************************************
|
||||
* GENERAL *
|
||||
************************************
|
||||
*/
|
||||
Header('General'),
|
||||
Option({
|
||||
opt: options.bar.customModules.scrollSpeed,
|
||||
title: 'Scrolling Speed',
|
||||
type: 'number',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* RAM *
|
||||
************************************
|
||||
*/
|
||||
Header('RAM'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.ram.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.icon,
|
||||
title: 'Ram Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.ram.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.labelType,
|
||||
title: 'Label Type',
|
||||
type: 'enum',
|
||||
enums: ['used/total', 'used', 'free', 'percentage'],
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.round,
|
||||
title: 'Round',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.ram.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* CPU *
|
||||
************************************
|
||||
*/
|
||||
Header('CPU'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpu.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.icon,
|
||||
title: 'Cpu Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpu.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.round,
|
||||
title: 'Round',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpu.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* CPU TEMP *
|
||||
************************************
|
||||
*/
|
||||
Header('CPU Temperature'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpuTemp.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.sensor,
|
||||
title: 'CPU Temperature Sensor',
|
||||
subtitle: 'Wiki: https://hyprpanel.com/configuration/panel.html#custom-modules',
|
||||
subtitleLink: 'https://hyprpanel.com/configuration/panel.html#custom-modules',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.unit,
|
||||
title: 'CPU Temperature Unit',
|
||||
type: 'enum',
|
||||
enums: ['imperial', 'metric'],
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.showUnit,
|
||||
title: 'Show Unit',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.icon,
|
||||
title: 'Cpu Temperature Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpuTemp.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.round,
|
||||
title: 'Round',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.cpuTemp.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* STORAGE *
|
||||
************************************
|
||||
*/
|
||||
Header('Storage'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.storage.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.icon,
|
||||
title: 'Storage Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.storage.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.labelType,
|
||||
title: 'Label Type',
|
||||
type: 'enum',
|
||||
enums: ['used/total', 'used', 'free', 'percentage'],
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.round,
|
||||
title: 'Round',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.storage.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* NETSTAT *
|
||||
************************************
|
||||
*/
|
||||
Header('Netstat'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.netstat.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.networkInterface,
|
||||
title: 'Network Interface',
|
||||
subtitle:
|
||||
"Name of the network interface to poll.\nHINT: Get list of interfaces with 'cat /proc/net/dev'",
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.dynamicIcon,
|
||||
title: 'Use Network Icon',
|
||||
subtitle: 'If enabled, shows the current network icon indicators instead of the static icon',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.icon,
|
||||
title: 'Netstat Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.rateUnit,
|
||||
title: 'Rate Unit',
|
||||
type: 'enum',
|
||||
enums: ['GiB', 'MiB', 'KiB', 'auto'],
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.netstat.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.labelType,
|
||||
title: 'Label Type',
|
||||
type: 'enum',
|
||||
enums: ['full', 'in', 'out'],
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.round,
|
||||
title: 'Round',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.netstat.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* KEYBOARD LAYOUT *
|
||||
************************************
|
||||
*/
|
||||
Header('Keyboard Layout'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.kbLayout.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.icon,
|
||||
title: 'Keyboard Layout Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.labelType,
|
||||
title: 'Label Type',
|
||||
type: 'enum',
|
||||
enums: ['layout', 'code'],
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.kbLayout.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.kbLayout.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* UPDATES *
|
||||
************************************
|
||||
*/
|
||||
Header('Updates'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.updates.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.updateCommand,
|
||||
title: 'Check Updates Command',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.icon,
|
||||
title: 'Updates Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.padZero,
|
||||
title: 'Pad with 0',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.updates.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
subtitle: "WARNING: Be careful of your package manager's rate limit.",
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.updates.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* SUBMAP *
|
||||
************************************
|
||||
*/
|
||||
Header('Submap'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.submap.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.showSubmapName,
|
||||
title: 'Show Submap Name',
|
||||
subtitle:
|
||||
'When enabled, the name of the current submap will be displayed' +
|
||||
' instead of the Submap Enabled or Disabled text.',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.enabledIcon,
|
||||
title: 'Enabled Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.disabledIcon,
|
||||
title: 'Disabled Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.enabledText,
|
||||
title: 'Enabled Text',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.disabledText,
|
||||
title: 'Disabled Text',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.submap.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.submap.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* WEATHER *
|
||||
************************************
|
||||
*/
|
||||
Header('Weather'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.weather.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.weather.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.weather.unit,
|
||||
title: 'Units',
|
||||
type: 'enum',
|
||||
enums: ['imperial', 'metric'],
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.weather.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.weather.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.weather.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.weather.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.weather.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.weather.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* HYPRSUNSET *
|
||||
************************************
|
||||
*/
|
||||
Header('Hyprsunset'),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.temperature,
|
||||
title: 'Temperature',
|
||||
subtitle: 'Ex: 1000k, 2000k, 5000k, etc.',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hyprsunset.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.onIcon,
|
||||
title: 'Enabled Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.offIcon,
|
||||
title: 'Disabled Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.onLabel,
|
||||
title: 'Enabled Label',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.offLabel,
|
||||
title: 'Disabled Label',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hyprsunset.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hyprsunset.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* HYPRIDLE *
|
||||
************************************
|
||||
*/
|
||||
Header('Hypridle'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hypridle.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.onIcon,
|
||||
title: 'Enabled Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.offIcon,
|
||||
title: 'Disabled Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.onLabel,
|
||||
title: 'Enabled Label',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.offLabel,
|
||||
title: 'Disabled Label',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.label,
|
||||
title: 'Show Label',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hypridle.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.pollingInterval,
|
||||
title: 'Polling Interval',
|
||||
type: 'number',
|
||||
min: 100,
|
||||
max: 60 * 24 * 1000,
|
||||
increment: 1000,
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.hypridle.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
|
||||
/*
|
||||
************************************
|
||||
* POWER *
|
||||
************************************
|
||||
*/
|
||||
Header('Power'),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.power.enableBorder,
|
||||
title: 'Button Border',
|
||||
type: 'boolean',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.power.spacing,
|
||||
title: 'Spacing',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.power.icon,
|
||||
title: 'Power Button Icon',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.power.leftClick,
|
||||
title: 'Left Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.power.rightClick,
|
||||
title: 'Right Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.power.middleClick,
|
||||
title: 'Middle Click',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.power.scrollUp,
|
||||
title: 'Scroll Up',
|
||||
type: 'string',
|
||||
}),
|
||||
Option({
|
||||
opt: options.bar.customModules.power.scrollDown,
|
||||
title: 'Scroll Down',
|
||||
type: 'string',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
import { module } from '../module';
|
||||
|
||||
import options from 'options';
|
||||
import Button from 'types/widgets/button';
|
||||
|
||||
// Utility Methods
|
||||
import { inputHandler } from 'customModules/utils';
|
||||
import { computeCPU } from './computeCPU';
|
||||
import { BarBoxChild } from 'lib/types/bar';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { FunctionPoller } from 'lib/poller/FunctionPoller';
|
||||
|
||||
// All the user configurable options for the cpu module that are needed
|
||||
const { label, round, leftClick, rightClick, middleClick, scrollUp, scrollDown, pollingInterval, icon } =
|
||||
options.bar.customModules.cpu;
|
||||
|
||||
export const cpuUsage = Variable(0);
|
||||
|
||||
// Instantiate the Poller class for CPU usage polling
|
||||
const cpuPoller = new FunctionPoller<number, []>(
|
||||
// Variable to poll and update with the result of the function passed in
|
||||
cpuUsage,
|
||||
// Variables that should trigger the polling function to update when they change
|
||||
[round.bind('value')],
|
||||
// Interval at which to poll
|
||||
pollingInterval.bind('value'),
|
||||
// Function to execute to get the network data
|
||||
computeCPU,
|
||||
);
|
||||
|
||||
cpuPoller.initialize('cpu');
|
||||
|
||||
export const Cpu = (): BarBoxChild => {
|
||||
const renderLabel = (cpuUsg: number, rnd: boolean): string => {
|
||||
return rnd ? `${Math.round(cpuUsg)}%` : `${cpuUsg.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const cpuModule = module({
|
||||
textIcon: icon.bind('value'),
|
||||
label: Utils.merge([cpuUsage.bind('value'), round.bind('value')], (cpuUsg, rnd) => {
|
||||
return renderLabel(cpuUsg, rnd);
|
||||
}),
|
||||
tooltipText: 'CPU',
|
||||
boxClass: 'cpu',
|
||||
showLabelBinding: label.bind('value'),
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return cpuModule;
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import GLib from 'gi://GLib?version=2.0';
|
||||
import { convertCelsiusToFahrenheit } from 'globals/weather';
|
||||
import { UnitType } from 'lib/types/weather';
|
||||
import options from 'options';
|
||||
import { Variable as VariableType } from 'types/variable';
|
||||
const { sensor } = options.bar.customModules.cpuTemp;
|
||||
|
||||
/**
|
||||
* Retrieves the current CPU temperature.
|
||||
* @returns CPU temperature in degrees Celsius
|
||||
*/
|
||||
export const getCPUTemperature = (round: VariableType<boolean>, unit: VariableType<UnitType>): number => {
|
||||
try {
|
||||
if (sensor.value.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const [success, tempInfoBytes] = GLib.file_get_contents(sensor.value);
|
||||
const tempInfo = new TextDecoder('utf-8').decode(tempInfoBytes);
|
||||
|
||||
if (!success || !tempInfoBytes) {
|
||||
console.error(`Failed to read ${sensor.value} or file content is null.`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let decimalTemp = parseInt(tempInfo, 10) / 1000;
|
||||
|
||||
if (unit.value === 'imperial') {
|
||||
decimalTemp = convertCelsiusToFahrenheit(decimalTemp);
|
||||
}
|
||||
|
||||
return round.value ? Math.round(decimalTemp) : parseFloat(decimalTemp.toFixed(2));
|
||||
} catch (error) {
|
||||
console.error('Error calculating CPU Temp:', error);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import options from 'options';
|
||||
|
||||
// Module initializer
|
||||
import { module } from '../module';
|
||||
|
||||
import Button from 'types/widgets/button';
|
||||
|
||||
// Utility Methods
|
||||
import { inputHandler } from 'customModules/utils';
|
||||
import { getCPUTemperature } from './helpers';
|
||||
import { BarBoxChild } from 'lib/types/bar';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { FunctionPoller } from 'lib/poller/FunctionPoller';
|
||||
import { Variable as VariableType } from 'types/variable';
|
||||
import { UnitType } from 'lib/types/weather';
|
||||
|
||||
// All the user configurable options for the cpu module that are needed
|
||||
const {
|
||||
label,
|
||||
sensor,
|
||||
round,
|
||||
showUnit,
|
||||
unit,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
scrollUp,
|
||||
scrollDown,
|
||||
pollingInterval,
|
||||
icon,
|
||||
} = options.bar.customModules.cpuTemp;
|
||||
|
||||
export const cpuTemp = Variable(0);
|
||||
|
||||
const cpuTempPoller = new FunctionPoller<number, [VariableType<boolean>, VariableType<UnitType>]>(
|
||||
// Variable to poll and update with the result of the function passed in
|
||||
cpuTemp,
|
||||
// Variables that should trigger the polling function to update when they change
|
||||
[sensor.bind('value'), round.bind('value'), unit.bind('value')],
|
||||
// Interval at which to poll
|
||||
pollingInterval.bind('value'),
|
||||
// Function to execute to get the network data
|
||||
getCPUTemperature,
|
||||
round,
|
||||
unit,
|
||||
);
|
||||
|
||||
cpuTempPoller.initialize('cputemp');
|
||||
|
||||
export const CpuTemp = (): BarBoxChild => {
|
||||
const cpuTempModule = module({
|
||||
textIcon: icon.bind('value'),
|
||||
label: Utils.merge(
|
||||
[cpuTemp.bind('value'), unit.bind('value'), showUnit.bind('value'), round.bind('value')],
|
||||
(cpuTmp, tempUnit, shwUnit) => {
|
||||
const unitLabel = tempUnit === 'imperial' ? 'F' : 'C';
|
||||
const unit = shwUnit ? ` ${unitLabel}` : '';
|
||||
|
||||
return `${cpuTmp.toString()}°${unit}`;
|
||||
},
|
||||
),
|
||||
tooltipText: 'CPU Temperature',
|
||||
boxClass: 'cpu-temp',
|
||||
showLabelBinding: label.bind('value'),
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return cpuTempModule;
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Variable as TVariable } from 'types/variable';
|
||||
|
||||
export const isActiveCommand = `bash -c "pgrep -x 'hypridle' &>/dev/null && echo 'yes' || echo 'no'"`;
|
||||
|
||||
export const isActive = Variable(false);
|
||||
|
||||
export const toggleIdle = (isActive: TVariable<boolean>): void => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
if (res === 'no') {
|
||||
Utils.execAsync(`bash -c "nohup hypridle > /dev/null 2>&1 &"`).then(() => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
isActive.value = res === 'yes';
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Utils.execAsync(`bash -c "pkill hypridle "`).then(() => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
isActive.value = res === 'yes';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const checkIdleStatus = (): undefined => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
isActive.value = res === 'yes';
|
||||
});
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import options from 'options';
|
||||
|
||||
import { Variable as TVariable } from 'types/variable';
|
||||
|
||||
const { temperature } = options.bar.customModules.hyprsunset;
|
||||
|
||||
export const isActiveCommand = `bash -c "pgrep -x 'hyprsunset' > /dev/null && echo 'yes' || echo 'no'"`;
|
||||
|
||||
export const isActive = Variable(false);
|
||||
|
||||
export const toggleSunset = (isActive: TVariable<boolean>): void => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
if (res === 'no') {
|
||||
Utils.execAsync(`bash -c "nohup hyprsunset -t ${temperature.value} > /dev/null 2>&1 &"`).then(() => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
isActive.value = res === 'yes';
|
||||
});
|
||||
});
|
||||
} else {
|
||||
Utils.execAsync(`bash -c "pkill hyprsunset "`).then(() => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
isActive.value = res === 'yes';
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const checkSunsetStatus = (): undefined => {
|
||||
Utils.execAsync(isActiveCommand).then((res) => {
|
||||
isActive.value = res === 'yes';
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -1,120 +0,0 @@
|
||||
const network = await Service.import('network');
|
||||
import options from 'options';
|
||||
import { module } from '../module';
|
||||
import { inputHandler } from 'customModules/utils';
|
||||
import { computeNetwork } from './computeNetwork';
|
||||
import { BarBoxChild, NetstatLabelType, RateUnit } from 'lib/types/bar';
|
||||
import Button from 'types/widgets/button';
|
||||
import { NetworkResourceData } from 'lib/types/customModules/network';
|
||||
import { NETWORK_LABEL_TYPES } from 'lib/types/defaults/bar';
|
||||
import { GET_DEFAULT_NETSTAT_DATA } from 'lib/types/defaults/netstat';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { FunctionPoller } from 'lib/poller/FunctionPoller';
|
||||
import { Variable as TVariable } from 'types/variable';
|
||||
|
||||
const {
|
||||
label,
|
||||
labelType,
|
||||
networkInterface,
|
||||
rateUnit,
|
||||
dynamicIcon,
|
||||
icon,
|
||||
round,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
pollingInterval,
|
||||
} = options.bar.customModules.netstat;
|
||||
|
||||
export const networkUsage = Variable<NetworkResourceData>(GET_DEFAULT_NETSTAT_DATA(rateUnit.value));
|
||||
|
||||
const netstatPoller = new FunctionPoller<
|
||||
NetworkResourceData,
|
||||
[round: TVariable<boolean>, interfaceNameVar: TVariable<string>, dataType: TVariable<RateUnit>]
|
||||
>(
|
||||
// Variable to poll and update with the result of the function passed in
|
||||
networkUsage,
|
||||
// Variables that should trigger the polling function to update when they change
|
||||
[rateUnit.bind('value'), networkInterface.bind('value'), round.bind('value')],
|
||||
// Interval at which to poll
|
||||
pollingInterval.bind('value'),
|
||||
// Function to execute to get the network data
|
||||
computeNetwork,
|
||||
// Optional parameters to pass to the function
|
||||
// round is a boolean that determines whether to round the values
|
||||
round,
|
||||
// Optional parameters to pass to the function
|
||||
// networkInterface is the interface name to filter the data
|
||||
networkInterface,
|
||||
// Optional parameters to pass to the function
|
||||
// rateUnit is the unit to display the data in
|
||||
// e.g. KiB, MiB, GiB, etc.
|
||||
rateUnit,
|
||||
);
|
||||
|
||||
netstatPoller.initialize('netstat');
|
||||
|
||||
export const Netstat = (): BarBoxChild => {
|
||||
const renderNetworkLabel = (lblType: NetstatLabelType, network: NetworkResourceData): string => {
|
||||
switch (lblType) {
|
||||
case 'in':
|
||||
return `↓ ${network.in}`;
|
||||
case 'out':
|
||||
return `↑ ${network.out}`;
|
||||
default:
|
||||
return `↓ ${network.in} ↑ ${network.out}`;
|
||||
}
|
||||
};
|
||||
|
||||
const netstatModule = module({
|
||||
useTextIcon: dynamicIcon.bind('value').as((useDynamicIcon) => !useDynamicIcon),
|
||||
icon: Utils.merge([network.bind('primary'), network.bind('wifi'), network.bind('wired')], (pmry, wfi, wrd) => {
|
||||
if (pmry === 'wired') {
|
||||
return wrd.icon_name;
|
||||
}
|
||||
return wfi.icon_name;
|
||||
}),
|
||||
textIcon: icon.bind('value'),
|
||||
label: Utils.merge(
|
||||
[networkUsage.bind('value'), labelType.bind('value')],
|
||||
(network: NetworkResourceData, lblTyp: NetstatLabelType) => renderNetworkLabel(lblTyp, network),
|
||||
),
|
||||
tooltipText: labelType.bind('value').as((lblTyp) => {
|
||||
return lblTyp === 'full' ? 'Ingress / Egress' : lblTyp === 'in' ? 'Ingress' : 'Egress';
|
||||
}),
|
||||
boxClass: 'netstat',
|
||||
showLabelBinding: label.bind('value'),
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
fn: () => {
|
||||
labelType.value = NETWORK_LABEL_TYPES[
|
||||
(NETWORK_LABEL_TYPES.indexOf(labelType.value) + 1) % NETWORK_LABEL_TYPES.length
|
||||
] as NetstatLabelType;
|
||||
},
|
||||
},
|
||||
onScrollDown: {
|
||||
fn: () => {
|
||||
labelType.value = NETWORK_LABEL_TYPES[
|
||||
(NETWORK_LABEL_TYPES.indexOf(labelType.value) - 1 + NETWORK_LABEL_TYPES.length) %
|
||||
NETWORK_LABEL_TYPES.length
|
||||
] as NetstatLabelType;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return netstatModule;
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import options from 'options';
|
||||
|
||||
// Module initializer
|
||||
import { module } from '../module';
|
||||
|
||||
// Types
|
||||
import { GenericResourceData } from 'lib/types/customModules/generic';
|
||||
import Button from 'types/widgets/button';
|
||||
|
||||
// Helper Methods
|
||||
import { calculateRamUsage } from './computeRam';
|
||||
|
||||
// Utility Methods
|
||||
import { formatTooltip, inputHandler, renderResourceLabel } from 'customModules/utils';
|
||||
import { BarBoxChild, ResourceLabelType } from 'lib/types/bar';
|
||||
|
||||
// Global Constants
|
||||
import { LABEL_TYPES } from 'lib/types/defaults/bar';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { FunctionPoller } from 'lib/poller/FunctionPoller';
|
||||
import { Variable as TVariable } from 'types/variable';
|
||||
|
||||
// All the user configurable options for the ram module that are needed
|
||||
const { label, labelType, round, leftClick, rightClick, middleClick, pollingInterval, icon } =
|
||||
options.bar.customModules.ram;
|
||||
|
||||
const defaultRamData: GenericResourceData = { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
const ramUsage = Variable<GenericResourceData>(defaultRamData);
|
||||
|
||||
const ramPoller = new FunctionPoller<GenericResourceData, [TVariable<boolean>]>(
|
||||
ramUsage,
|
||||
[round.bind('value')],
|
||||
pollingInterval.bind('value'),
|
||||
calculateRamUsage,
|
||||
round,
|
||||
);
|
||||
|
||||
ramPoller.initialize('ram');
|
||||
|
||||
export const Ram = (): BarBoxChild => {
|
||||
const ramModule = module({
|
||||
textIcon: icon.bind('value'),
|
||||
label: Utils.merge(
|
||||
[ramUsage.bind('value'), labelType.bind('value'), round.bind('value')],
|
||||
(rmUsg: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
|
||||
const returnValue = renderResourceLabel(lblTyp, rmUsg, round);
|
||||
|
||||
return returnValue;
|
||||
},
|
||||
),
|
||||
tooltipText: labelType.bind('value').as((lblTyp) => {
|
||||
return formatTooltip('RAM', lblTyp);
|
||||
}),
|
||||
boxClass: 'ram',
|
||||
showLabelBinding: label.bind('value'),
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
fn: () => {
|
||||
labelType.value = LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.value) + 1) % LABEL_TYPES.length
|
||||
] as ResourceLabelType;
|
||||
},
|
||||
},
|
||||
onScrollDown: {
|
||||
fn: () => {
|
||||
labelType.value = LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.value) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
|
||||
] as ResourceLabelType;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return ramModule;
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
// @ts-expect-error is a special directive that tells the compiler to use the GTop library
|
||||
import GTop from 'gi://GTop';
|
||||
|
||||
import { divide } from 'customModules/utils';
|
||||
import { Variable as VariableType } from 'types/variable';
|
||||
import { GenericResourceData } from 'lib/types/customModules/generic';
|
||||
|
||||
// FIX: Consolidate with Storage service class
|
||||
export const computeStorage = (round: VariableType<boolean>): GenericResourceData => {
|
||||
try {
|
||||
const currentFsUsage = new GTop.glibtop_fsusage();
|
||||
|
||||
GTop.glibtop_get_fsusage(currentFsUsage, '/');
|
||||
|
||||
const total = currentFsUsage.blocks * currentFsUsage.block_size;
|
||||
const available = currentFsUsage.bavail * currentFsUsage.block_size;
|
||||
const used = total - available;
|
||||
|
||||
return {
|
||||
total,
|
||||
used,
|
||||
free: available,
|
||||
percentage: divide([total, used], round.value),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error calculating RAM usage:', error);
|
||||
return { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
}
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import options from 'options';
|
||||
import { module } from '../module';
|
||||
import { formatTooltip, inputHandler, renderResourceLabel } from 'customModules/utils';
|
||||
import { computeStorage } from './computeStorage';
|
||||
import { BarBoxChild, ResourceLabelType } from 'lib/types/bar';
|
||||
import { GenericResourceData } from 'lib/types/customModules/generic';
|
||||
import Button from 'types/widgets/button';
|
||||
import { LABEL_TYPES } from 'lib/types/defaults/bar';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { FunctionPoller } from 'lib/poller/FunctionPoller';
|
||||
import { Variable as TVariable } from 'types/variable';
|
||||
|
||||
const { label, labelType, icon, round, leftClick, rightClick, middleClick, pollingInterval } =
|
||||
options.bar.customModules.storage;
|
||||
|
||||
const defaultStorageData = { total: 0, used: 0, percentage: 0, free: 0 };
|
||||
|
||||
const storageUsage = Variable<GenericResourceData>(defaultStorageData);
|
||||
|
||||
const storagePoller = new FunctionPoller<GenericResourceData, [TVariable<boolean>]>(
|
||||
storageUsage,
|
||||
[round.bind('value')],
|
||||
pollingInterval.bind('value'),
|
||||
computeStorage,
|
||||
round,
|
||||
);
|
||||
|
||||
storagePoller.initialize('storage');
|
||||
|
||||
export const Storage = (): BarBoxChild => {
|
||||
const storageModule = module({
|
||||
textIcon: icon.bind('value'),
|
||||
label: Utils.merge(
|
||||
[storageUsage.bind('value'), labelType.bind('value'), round.bind('value')],
|
||||
(storage: GenericResourceData, lblTyp: ResourceLabelType, round: boolean) => {
|
||||
return renderResourceLabel(lblTyp, storage, round);
|
||||
},
|
||||
),
|
||||
tooltipText: labelType.bind('value').as((lblTyp) => {
|
||||
return formatTooltip('Storage', lblTyp);
|
||||
}),
|
||||
boxClass: 'storage',
|
||||
showLabelBinding: label.bind('value'),
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
fn: () => {
|
||||
labelType.value = LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.value) + 1) % LABEL_TYPES.length
|
||||
] as ResourceLabelType;
|
||||
},
|
||||
},
|
||||
onScrollDown: {
|
||||
fn: () => {
|
||||
labelType.value = LABEL_TYPES[
|
||||
(LABEL_TYPES.indexOf(labelType.value) - 1 + LABEL_TYPES.length) % LABEL_TYPES.length
|
||||
] as ResourceLabelType;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return storageModule;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Variable } from 'types/variable';
|
||||
|
||||
const hyprland = await Service.import('hyprland');
|
||||
|
||||
export const isSubmapEnabled = (submap: string, enabled: string, disabled: string): string => {
|
||||
return submap !== 'default' ? enabled : disabled;
|
||||
};
|
||||
|
||||
export const getInitialSubmap = (submapStatus: Variable<string>): void => {
|
||||
let submap = hyprland.message('submap');
|
||||
|
||||
const newLineCarriage = /\n/g;
|
||||
submap = submap.replace(newLineCarriage, '');
|
||||
|
||||
if (submap === 'unknown request') {
|
||||
submap = 'default';
|
||||
}
|
||||
|
||||
submapStatus.value = submap;
|
||||
};
|
||||
@@ -1,101 +0,0 @@
|
||||
const hyprland = await Service.import('hyprland');
|
||||
import options from 'options';
|
||||
import { module } from '../module';
|
||||
|
||||
import { inputHandler } from 'customModules/utils';
|
||||
import Button from 'types/widgets/button';
|
||||
import { Variable as VariableType } from 'types/variable';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { BarBoxChild } from 'lib/types/bar';
|
||||
import { capitalizeFirstLetter } from 'lib/utils';
|
||||
import { getInitialSubmap, isSubmapEnabled } from './helpers';
|
||||
|
||||
const {
|
||||
label,
|
||||
showSubmapName,
|
||||
enabledIcon,
|
||||
disabledIcon,
|
||||
enabledText,
|
||||
disabledText,
|
||||
leftClick,
|
||||
rightClick,
|
||||
middleClick,
|
||||
scrollUp,
|
||||
scrollDown,
|
||||
} = options.bar.customModules.submap;
|
||||
|
||||
const submapStatus: VariableType<string> = Variable('default');
|
||||
|
||||
hyprland.connect('submap', (_, currentSubmap) => {
|
||||
if (currentSubmap.length === 0) {
|
||||
submapStatus.value = 'default';
|
||||
} else {
|
||||
submapStatus.value = currentSubmap;
|
||||
}
|
||||
});
|
||||
|
||||
getInitialSubmap(submapStatus);
|
||||
|
||||
export const Submap = (): BarBoxChild => {
|
||||
const submapModule = module({
|
||||
textIcon: Utils.merge(
|
||||
[submapStatus.bind('value'), enabledIcon.bind('value'), disabledIcon.bind('value')],
|
||||
(status, enabled, disabled) => {
|
||||
return isSubmapEnabled(status, enabled, disabled);
|
||||
},
|
||||
),
|
||||
tooltipText: Utils.merge(
|
||||
[
|
||||
submapStatus.bind('value'),
|
||||
enabledText.bind('value'),
|
||||
disabledText.bind('value'),
|
||||
showSubmapName.bind('value'),
|
||||
],
|
||||
(status, enabled, disabled, showSmName) => {
|
||||
if (showSmName) {
|
||||
return capitalizeFirstLetter(status);
|
||||
}
|
||||
return isSubmapEnabled(status, enabled, disabled);
|
||||
},
|
||||
),
|
||||
boxClass: 'submap',
|
||||
label: Utils.merge(
|
||||
[
|
||||
submapStatus.bind('value'),
|
||||
enabledText.bind('value'),
|
||||
disabledText.bind('value'),
|
||||
showSubmapName.bind('value'),
|
||||
],
|
||||
(status, enabled, disabled, showSmName) => {
|
||||
if (showSmName) {
|
||||
return capitalizeFirstLetter(status);
|
||||
}
|
||||
return isSubmapEnabled(status, enabled, disabled);
|
||||
},
|
||||
),
|
||||
showLabelBinding: label.bind('value'),
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return submapModule;
|
||||
};
|
||||
@@ -1,222 +0,0 @@
|
||||
import { Option } from 'widget/settings/shared/Option';
|
||||
import { Header } from 'widget/settings/shared/Header';
|
||||
|
||||
import options from 'options';
|
||||
import Scrollable from 'types/widgets/scrollable';
|
||||
import { Attribute, GtkWidget } from 'lib/types/widget';
|
||||
|
||||
export const CustomModuleTheme = (): Scrollable<GtkWidget, Attribute> => {
|
||||
return Widget.Scrollable({
|
||||
vscroll: 'automatic',
|
||||
hscroll: 'automatic',
|
||||
class_name: 'menu-theme-page customModules paged-container',
|
||||
child: Widget.Box({
|
||||
class_name: 'bar-theme-page paged-container',
|
||||
vertical: true,
|
||||
children: [
|
||||
Header('RAM'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.ram.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.ram.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.ram.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.ram.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.ram.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('CPU'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.cpu.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.cpu.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpu.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpu.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.cpu.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('CPU Temperature'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.cpuTemp.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.cpuTemp.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpuTemp.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.cpuTemp.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.cpuTemp.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Storage'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.storage.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.storage.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.storage.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.storage.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.storage.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Netstat'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.netstat.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.netstat.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.netstat.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.netstat.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.netstat.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Keyboard Layout'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.kbLayout.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.kbLayout.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.kbLayout.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.kbLayout.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.kbLayout.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Updates'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.updates.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.updates.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.updates.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.updates.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.updates.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Submap'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.submap.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.submap.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.submap.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.submap.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.submap.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Weather'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.weather.icon, title: 'Icon', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.weather.text, title: 'Text', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.weather.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.weather.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.weather.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Hyprsunset'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.hyprsunset.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.hyprsunset.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hyprsunset.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hyprsunset.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.hyprsunset.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Hypridle'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.hypridle.text, title: 'Text', type: 'color' }),
|
||||
Option({ opt: options.theme.bar.buttons.modules.hypridle.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hypridle.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.hypridle.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.hypridle.border, title: 'Border', type: 'color' }),
|
||||
|
||||
Header('Power'),
|
||||
Option({ opt: options.theme.bar.buttons.modules.power.icon, title: 'Icon', type: 'color' }),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.power.background,
|
||||
title: 'Label Background',
|
||||
type: 'color',
|
||||
}),
|
||||
Option({
|
||||
opt: options.theme.bar.buttons.modules.power.icon_background,
|
||||
title: 'Icon Background',
|
||||
subtitle:
|
||||
"Applies a background color to the icon section of the button.\nRequires 'split' button styling.",
|
||||
type: 'color',
|
||||
}),
|
||||
Option({ opt: options.theme.bar.buttons.modules.power.border, title: 'Border', type: 'color' }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -1,238 +0,0 @@
|
||||
import { ResourceLabelType } from 'lib/types/bar';
|
||||
import { GenericResourceData, Postfix } from 'lib/types/customModules/generic';
|
||||
import { InputHandlerEvents, RunAsyncCommand } from 'lib/types/customModules/utils';
|
||||
import { ThrottleFn, ThrottleFnCallback } from 'lib/types/utils';
|
||||
import { Attribute, Child, EventArgs } from 'lib/types/widget';
|
||||
import { Binding } from 'lib/utils';
|
||||
import { openMenu } from 'modules/bar/utils';
|
||||
import options from 'options';
|
||||
import Gdk from 'types/@girs/gdk-3.0/gdk-3.0';
|
||||
import { Variable as VariableType } from 'types/variable';
|
||||
import Button from 'types/widgets/button';
|
||||
|
||||
const { scrollSpeed } = options.bar.customModules;
|
||||
|
||||
const handlePostInputUpdater = (postInputUpdater?: VariableType<boolean>): void => {
|
||||
if (postInputUpdater !== undefined) {
|
||||
postInputUpdater.value = !postInputUpdater.value;
|
||||
}
|
||||
};
|
||||
|
||||
export const runAsyncCommand: RunAsyncCommand = (cmd, events, fn, postInputUpdater?: VariableType<boolean>): void => {
|
||||
if (cmd.startsWith('menu:')) {
|
||||
const menuName = cmd.split(':')[1].trim().toLowerCase();
|
||||
openMenu(events.clicked, events.event, `${menuName}menu`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Utils.execAsync(`bash -c "${cmd}"`)
|
||||
.then((output) => {
|
||||
handlePostInputUpdater(postInputUpdater);
|
||||
if (fn !== undefined) {
|
||||
fn(output);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error(`Error running command "${cmd}": ${err})`));
|
||||
};
|
||||
|
||||
export function throttleInput<T extends ThrottleFn>(func: T, limit: number): T {
|
||||
let inThrottle: boolean;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export const throttledScrollHandler = (interval: number): ThrottleFn =>
|
||||
throttleInput(
|
||||
(cmd: string, events: EventArgs, fn: ThrottleFnCallback, postInputUpdater?: VariableType<boolean>) => {
|
||||
runAsyncCommand(cmd, events, fn, postInputUpdater);
|
||||
},
|
||||
200 / interval,
|
||||
);
|
||||
|
||||
const dummyVar = Variable('');
|
||||
|
||||
export const inputHandler = (
|
||||
self: Button<Child, Attribute>,
|
||||
{ onPrimaryClick, onSecondaryClick, onMiddleClick, onScrollUp, onScrollDown }: InputHandlerEvents,
|
||||
postInputUpdater?: VariableType<boolean>,
|
||||
): void => {
|
||||
const sanitizeInput = (input: VariableType<string>): string => {
|
||||
if (input === undefined) {
|
||||
return '';
|
||||
}
|
||||
return input.value;
|
||||
};
|
||||
|
||||
const updateHandlers = (): void => {
|
||||
const interval = scrollSpeed.value;
|
||||
const throttledHandler = throttledScrollHandler(interval);
|
||||
|
||||
self.on_primary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(
|
||||
sanitizeInput(onPrimaryClick?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onPrimaryClick.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
};
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(
|
||||
sanitizeInput(onSecondaryClick?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onSecondaryClick.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
};
|
||||
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(
|
||||
sanitizeInput(onMiddleClick?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onMiddleClick.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
};
|
||||
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(
|
||||
sanitizeInput(onScrollUp?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onScrollUp.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
};
|
||||
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(
|
||||
sanitizeInput(onScrollDown?.cmd || dummyVar),
|
||||
{ clicked, event },
|
||||
onScrollDown.fn,
|
||||
postInputUpdater,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
// Initial setup of event handlers
|
||||
updateHandlers();
|
||||
|
||||
const sanitizeVariable = (someVar: VariableType<string> | undefined): Binding<string> => {
|
||||
if (someVar === undefined || typeof someVar.bind !== 'function') {
|
||||
return dummyVar.bind('value');
|
||||
}
|
||||
return someVar.bind('value');
|
||||
};
|
||||
|
||||
// Re-run the update whenever scrollSpeed changes
|
||||
Utils.merge(
|
||||
[
|
||||
scrollSpeed.bind('value'),
|
||||
sanitizeVariable(onPrimaryClick),
|
||||
sanitizeVariable(onSecondaryClick),
|
||||
sanitizeVariable(onMiddleClick),
|
||||
sanitizeVariable(onScrollUp),
|
||||
sanitizeVariable(onScrollDown),
|
||||
],
|
||||
updateHandlers,
|
||||
);
|
||||
};
|
||||
|
||||
export const divide = ([total, used]: number[], round: boolean): number => {
|
||||
const percentageTotal = (used / total) * 100;
|
||||
if (round) {
|
||||
return total > 0 ? Math.round(percentageTotal) : 0;
|
||||
}
|
||||
return total > 0 ? parseFloat(percentageTotal.toFixed(2)) : 0;
|
||||
};
|
||||
|
||||
export const formatSizeInKiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 1;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
export const formatSizeInMiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 2;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
export const formatSizeInGiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 3;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
export const formatSizeInTiB = (sizeInBytes: number, round: boolean): number => {
|
||||
const sizeInGiB = sizeInBytes / 1024 ** 4;
|
||||
return round ? Math.round(sizeInGiB) : parseFloat(sizeInGiB.toFixed(2));
|
||||
};
|
||||
|
||||
export const autoFormatSize = (sizeInBytes: number, round: boolean): number => {
|
||||
// auto convert to GiB, MiB, KiB, TiB, or bytes
|
||||
if (sizeInBytes >= 1024 ** 4) return formatSizeInTiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 3) return formatSizeInGiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 2) return formatSizeInMiB(sizeInBytes, round);
|
||||
if (sizeInBytes >= 1024 ** 1) return formatSizeInKiB(sizeInBytes, round);
|
||||
|
||||
return sizeInBytes;
|
||||
};
|
||||
|
||||
export const getPostfix = (sizeInBytes: number): Postfix => {
|
||||
if (sizeInBytes >= 1024 ** 4) return 'TiB';
|
||||
if (sizeInBytes >= 1024 ** 3) return 'GiB';
|
||||
if (sizeInBytes >= 1024 ** 2) return 'MiB';
|
||||
if (sizeInBytes >= 1024 ** 1) return 'KiB';
|
||||
|
||||
return 'B';
|
||||
};
|
||||
|
||||
export const renderResourceLabel = (lblType: ResourceLabelType, rmUsg: GenericResourceData, round: boolean): string => {
|
||||
const { used, total, percentage, free } = rmUsg;
|
||||
|
||||
const formatFunctions = {
|
||||
TiB: formatSizeInTiB,
|
||||
GiB: formatSizeInGiB,
|
||||
MiB: formatSizeInMiB,
|
||||
KiB: formatSizeInKiB,
|
||||
B: (size: number): number => size,
|
||||
};
|
||||
|
||||
// Get the data in proper GiB, MiB, KiB, TiB, or bytes
|
||||
const totalSizeFormatted = autoFormatSize(total, round);
|
||||
// get the postfix: one of [TiB, GiB, MiB, KiB, B]
|
||||
const postfix = getPostfix(total);
|
||||
|
||||
// Determine which format function to use
|
||||
const formatUsed = formatFunctions[postfix] || formatFunctions['B'];
|
||||
const usedSizeFormatted = formatUsed(used, round);
|
||||
|
||||
if (lblType === 'used/total') {
|
||||
return `${usedSizeFormatted}/${totalSizeFormatted} ${postfix}`;
|
||||
}
|
||||
if (lblType === 'used') {
|
||||
return `${autoFormatSize(used, round)} ${getPostfix(used)}`;
|
||||
}
|
||||
if (lblType === 'free') {
|
||||
return `${autoFormatSize(free, round)} ${getPostfix(free)}`;
|
||||
}
|
||||
|
||||
return `${percentage}%`;
|
||||
};
|
||||
|
||||
export const formatTooltip = (dataType: string, lblTyp: ResourceLabelType): string => {
|
||||
switch (lblTyp) {
|
||||
case 'used':
|
||||
return `Used ${dataType}`;
|
||||
case 'free':
|
||||
return `Free ${dataType}`;
|
||||
case 'used/total':
|
||||
return `Used/Total ${dataType}`;
|
||||
case 'percentage':
|
||||
return `Percentage ${dataType} Usage`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
import options from 'options';
|
||||
import { module } from '../module';
|
||||
|
||||
import { inputHandler } from 'customModules/utils';
|
||||
import Button from 'types/widgets/button';
|
||||
import { getWeatherStatusTextIcon, globalWeatherVar } from 'globals/weather';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { BarBoxChild } from 'lib/types/bar';
|
||||
|
||||
const { label, unit, leftClick, rightClick, middleClick, scrollUp, scrollDown } = options.bar.customModules.weather;
|
||||
|
||||
export const Weather = (): BarBoxChild => {
|
||||
const weatherModule = module({
|
||||
textIcon: Utils.merge([globalWeatherVar.bind('value')], (wthr) => {
|
||||
const weatherStatusIcon = getWeatherStatusTextIcon(wthr);
|
||||
return weatherStatusIcon;
|
||||
}),
|
||||
tooltipText: globalWeatherVar.bind('value').as((v) => `Weather Status: ${v.current.condition.text}`),
|
||||
boxClass: 'weather-custom',
|
||||
label: Utils.merge([globalWeatherVar.bind('value'), unit.bind('value')], (wthr, unt) => {
|
||||
if (unt === 'imperial') {
|
||||
return `${Math.ceil(wthr.current.temp_f)}° F`;
|
||||
} else {
|
||||
return `${Math.ceil(wthr.current.temp_c)}° C`;
|
||||
}
|
||||
}),
|
||||
showLabelBinding: label.bind('value'),
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>) => {
|
||||
inputHandler(self, {
|
||||
onPrimaryClick: {
|
||||
cmd: leftClick,
|
||||
},
|
||||
onSecondaryClick: {
|
||||
cmd: rightClick,
|
||||
},
|
||||
onMiddleClick: {
|
||||
cmd: middleClick,
|
||||
},
|
||||
onScrollUp: {
|
||||
cmd: scrollUp,
|
||||
},
|
||||
onScrollDown: {
|
||||
cmd: scrollDown,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return weatherModule;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js';
|
||||
import App from 'resource:///com/github/Aylur/ags/app.js';
|
||||
import { monitorFile } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
import Gio from 'gi://Gio';
|
||||
import { FileInfo } from 'types/@girs/gio-2.0/gio-2.0.cjs';
|
||||
|
||||
class DirectoryMonitorService extends Service {
|
||||
static {
|
||||
Service.register(this, {}, {});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.recursiveDirectoryMonitor(`${App.configDir}/scss`);
|
||||
}
|
||||
|
||||
recursiveDirectoryMonitor(directoryPath: string): void {
|
||||
monitorFile(directoryPath, (_, eventType) => {
|
||||
if (eventType === Gio.FileMonitorEvent.CHANGES_DONE_HINT) {
|
||||
this.emit('changed');
|
||||
}
|
||||
});
|
||||
|
||||
const directory = Gio.File.new_for_path(directoryPath);
|
||||
const enumerator = directory.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null);
|
||||
|
||||
let fileInfo: FileInfo;
|
||||
while ((fileInfo = enumerator.next_file(null) as FileInfo) !== null) {
|
||||
const childPath = directoryPath + '/' + fileInfo.get_name();
|
||||
if (fileInfo.get_file_type() === Gio.FileType.DIRECTORY) {
|
||||
this.recursiveDirectoryMonitor(childPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const service = new DirectoryMonitorService();
|
||||
export default service;
|
||||
21
env.d.ts
vendored
Normal file
21
env.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
declare const SRC: string;
|
||||
|
||||
declare module 'inline:*' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.scss' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.blp' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.css' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
1
external/ags-types
vendored
1
external/ags-types
vendored
Submodule external/ags-types deleted from 87b5046791
@@ -1,6 +0,0 @@
|
||||
import { Variable as VariableType } from 'types/variable';
|
||||
|
||||
type GlobalEventBoxes = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
export const globalEventBoxes: VariableType<GlobalEventBoxes> = Variable({});
|
||||
@@ -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;
|
||||
@@ -1,46 +0,0 @@
|
||||
const notifs = await Service.import('notifications');
|
||||
import icons from 'modules/icons/index';
|
||||
import options from 'options';
|
||||
import { Notification } from 'types/service/notifications';
|
||||
|
||||
const { clearDelay } = options.notifications;
|
||||
|
||||
export const removingNotifications = Variable<boolean>(false);
|
||||
|
||||
export const getNotificationIcon = (app_name: string, app_icon: string, app_entry: string): string => {
|
||||
let icon: string = icons.fallback.notification;
|
||||
|
||||
if (Utils.lookUpIcon(app_name) || Utils.lookUpIcon(app_name.toLowerCase() || '')) {
|
||||
icon = Utils.lookUpIcon(app_name)
|
||||
? app_name
|
||||
: Utils.lookUpIcon(app_name.toLowerCase())
|
||||
? app_name.toLowerCase()
|
||||
: '';
|
||||
}
|
||||
|
||||
if (Utils.lookUpIcon(app_icon) && icon === '') {
|
||||
icon = app_icon;
|
||||
}
|
||||
|
||||
if (Utils.lookUpIcon(app_entry || '') && icon === '') {
|
||||
icon = app_entry || '';
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const clearNotifications = async (notifications: Notification[], delay: number): Promise<void> => {
|
||||
removingNotifications.value = true;
|
||||
for (const notif of notifications) {
|
||||
notif.close();
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
}
|
||||
removingNotifications.value = false;
|
||||
};
|
||||
|
||||
const clearAllNotifications = async (): Promise<void> => {
|
||||
clearNotifications(notifs.notifications, clearDelay.value);
|
||||
};
|
||||
|
||||
globalThis['removingNotifications'] = removingNotifications;
|
||||
globalThis['clearAllNotifications'] = clearAllNotifications;
|
||||
@@ -1,7 +0,0 @@
|
||||
const systemtray = await Service.import('systemtray');
|
||||
|
||||
globalThis.getSystrayItems = (): string => {
|
||||
return systemtray.items.map((systrayItem) => systrayItem.id).join('\n');
|
||||
};
|
||||
|
||||
export { getSystrayItems };
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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}`;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
147
lib/option.ts
147
lib/option.ts
@@ -1,147 +0,0 @@
|
||||
import { isHexColor } from 'globals/variables';
|
||||
import { Variable } from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
import { MkOptionsResult } from './types/options';
|
||||
|
||||
type OptProps = {
|
||||
persistent?: boolean;
|
||||
};
|
||||
|
||||
export class Opt<T = unknown> extends Variable<T> {
|
||||
static {
|
||||
Service.register(this);
|
||||
}
|
||||
|
||||
constructor(initial: T, { persistent = false }: OptProps = {}) {
|
||||
super(initial);
|
||||
this.initial = initial;
|
||||
this.persistent = persistent;
|
||||
}
|
||||
|
||||
initial: T;
|
||||
id = '';
|
||||
persistent: boolean;
|
||||
toString(): string {
|
||||
return `${this.value}`;
|
||||
}
|
||||
toJSON(): string {
|
||||
return `opt:${this.value}`;
|
||||
}
|
||||
|
||||
getValue = (): T => {
|
||||
return super.getValue();
|
||||
};
|
||||
init(cacheFile: string): void {
|
||||
const cacheV = JSON.parse(Utils.readFile(cacheFile) || '{}')[this.id];
|
||||
if (cacheV !== undefined) this.value = cacheV;
|
||||
|
||||
this.connect('changed', () => {
|
||||
const cache = JSON.parse(Utils.readFile(cacheFile) || '{}');
|
||||
cache[this.id] = this.value;
|
||||
Utils.writeFileSync(JSON.stringify(cache, null, 2), cacheFile);
|
||||
});
|
||||
}
|
||||
|
||||
reset(): string | undefined {
|
||||
if (this.persistent) return;
|
||||
|
||||
if (JSON.stringify(this.value) !== JSON.stringify(this.initial)) {
|
||||
this.value = this.initial;
|
||||
return this.id;
|
||||
}
|
||||
}
|
||||
|
||||
doResetColor(): string | undefined {
|
||||
if (this.persistent) return;
|
||||
|
||||
const isColor = isHexColor(this.value as string);
|
||||
if (JSON.stringify(this.value) !== JSON.stringify(this.initial) && isColor) {
|
||||
this.value = this.initial;
|
||||
return this.id;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const opt = <T>(initial: T, opts?: OptProps): Opt<T> => new Opt(initial, opts);
|
||||
|
||||
const getOptions = (object: Record<string, unknown>, path = ''): Opt[] => {
|
||||
return Object.keys(object).flatMap((key) => {
|
||||
const obj = object[key];
|
||||
const id = path ? path + '.' + key : key;
|
||||
|
||||
if (obj instanceof Variable) {
|
||||
const optValue = obj as Opt;
|
||||
optValue.id = id;
|
||||
return optValue;
|
||||
}
|
||||
|
||||
if (typeof obj === 'object' && obj !== null) {
|
||||
return getOptions(obj as Record<string, unknown>, id); // Recursively process nested objects
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
export function mkOptions<T extends object>(
|
||||
cacheFile: string,
|
||||
object: T,
|
||||
confFile: string = 'config.json',
|
||||
): T & MkOptionsResult {
|
||||
for (const opt of getOptions(object as Record<string, unknown>)) opt.init(cacheFile);
|
||||
|
||||
Utils.ensureDirectory(cacheFile.split('/').slice(0, -1).join('/'));
|
||||
|
||||
const configFile = `${TMP}/${confFile}`;
|
||||
const values = getOptions(object as Record<string, unknown>).reduce(
|
||||
(obj, { id, value }) => ({ [id]: value, ...obj }),
|
||||
{},
|
||||
);
|
||||
Utils.writeFileSync(JSON.stringify(values, null, 2), configFile);
|
||||
Utils.monitorFile(configFile, () => {
|
||||
const cache = JSON.parse(Utils.readFile(configFile) || '{}');
|
||||
for (const opt of getOptions(object as Record<string, unknown>)) {
|
||||
if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value)) opt.value = cache[opt.id];
|
||||
}
|
||||
});
|
||||
|
||||
function sleep(ms = 0): Promise<T> {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
const reset = async (
|
||||
[opt, ...list] = getOptions(object as Record<string, unknown>),
|
||||
id = opt?.reset(),
|
||||
): Promise<Array<string>> => {
|
||||
if (!opt) return sleep().then(() => []);
|
||||
|
||||
return id ? [id, ...(await sleep(50).then(() => reset(list)))] : await sleep().then(() => reset(list));
|
||||
};
|
||||
|
||||
const resetTheme = async (
|
||||
[opt, ...list] = getOptions(object as Record<string, unknown>),
|
||||
id = opt?.doResetColor(),
|
||||
): Promise<Array<string>> => {
|
||||
if (!opt) return sleep().then(() => []);
|
||||
|
||||
return id
|
||||
? [id, ...(await sleep(50).then(() => resetTheme(list)))]
|
||||
: await sleep().then(() => resetTheme(list));
|
||||
};
|
||||
|
||||
return Object.assign(object, {
|
||||
configFile,
|
||||
array: () => getOptions(object as Record<string, unknown>),
|
||||
async reset() {
|
||||
return (await reset()).join('\n');
|
||||
},
|
||||
async resetTheme() {
|
||||
return (await resetTheme()).join('\n');
|
||||
},
|
||||
handler(deps: string[], callback: () => void) {
|
||||
for (const opt of getOptions(object as Record<string, unknown>)) {
|
||||
if (deps.some((i) => opt.id.startsWith(i))) opt.connect('changed', callback);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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`);
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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'];
|
||||
12
lib/types/dropdownmenu.d.ts
vendored
12
lib/types/dropdownmenu.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
import { WindowProps } from 'types/widgets/window';
|
||||
import { GtkWidget, Transition } from './widget';
|
||||
import { Binding } from 'types/service';
|
||||
|
||||
export type DropdownMenuProps = {
|
||||
name: string;
|
||||
child: GtkWidget;
|
||||
layout?: string;
|
||||
transition?: Transition | Binding<Transition>;
|
||||
exclusivity?: Exclusivity;
|
||||
fixed?: boolean;
|
||||
} & WindowProps;
|
||||
2
lib/types/mpris.d.ts
vendored
2
lib/types/mpris.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export type LoopStatus = 'none' | 'track' | 'playlist';
|
||||
export type PlaybackStatus = 'playing' | 'paused' | 'stopped';
|
||||
29
lib/types/widget.d.ts
vendored
29
lib/types/widget.d.ts
vendored
@@ -1,29 +0,0 @@
|
||||
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0';
|
||||
import Box from 'types/widgets/box';
|
||||
|
||||
export type Exclusivity = 'normal' | 'ignore' | 'exclusive';
|
||||
export type Anchor = 'left' | 'right' | 'top' | 'down';
|
||||
export type Transition = 'none' | 'crossfade' | 'slide_right' | 'slide_left' | 'slide_up' | 'slide_down';
|
||||
|
||||
export type Layouts =
|
||||
| 'center'
|
||||
| 'top'
|
||||
| 'top-right'
|
||||
| 'top-center'
|
||||
| 'top-left'
|
||||
| 'bottom-left'
|
||||
| 'bottom-center'
|
||||
| 'bottom-right';
|
||||
|
||||
export type Attribute = unknown;
|
||||
export type Child = Gtk.Widget;
|
||||
export type GtkWidget = Gtk.Widget;
|
||||
export type BoxWidget = Box<GtkWidget, Child>;
|
||||
|
||||
export type GButton = Gtk.Button;
|
||||
export type GBox = Gtk.Box;
|
||||
export type GLabel = Gtk.Label;
|
||||
export type GCenterBox = Gtk.Box;
|
||||
|
||||
export type EventHandler<Self> = (self: Self, event: Gdk.Event) => boolean | unknown;
|
||||
export type EventArgs = { clicked: Button<Child, Attribute>; event: Gdk.Event };
|
||||
246
lib/utils.ts
246
lib/utils.ts
@@ -1,246 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { type Application } from 'types/service/applications';
|
||||
import { BarModule, NotificationAnchor } from './types/options';
|
||||
import { OSDAnchor } from 'lib/types/options';
|
||||
import icons, { substitutes } from './icons';
|
||||
import Gtk from 'gi://Gtk?version=3.0';
|
||||
import Gdk from 'gi://Gdk';
|
||||
import GLib from 'gi://GLib?version=2.0';
|
||||
import GdkPixbuf from 'gi://GdkPixbuf';
|
||||
import { NotificationArgs } from 'types/utils/notify';
|
||||
import { SubstituteKeys } from './types/utils';
|
||||
import { Window } from 'types/@girs/gtk-3.0/gtk-3.0.cjs';
|
||||
import { namedColors } from './constants/colors';
|
||||
import { distroIcons } from './constants/distro';
|
||||
import { distro } from './variables';
|
||||
const battery = await Service.import('battery');
|
||||
import options from 'options';
|
||||
|
||||
export type Binding<T> = import('types/service').Binding<any, any, T>;
|
||||
|
||||
/**
|
||||
* Retrieves all unique layout items from the bar options.
|
||||
*
|
||||
* @returns An array of unique layout items.
|
||||
*/
|
||||
export const getLayoutItems = (): BarModule[] => {
|
||||
const { layouts } = options.bar;
|
||||
|
||||
const itemsInLayout: BarModule[] = [];
|
||||
|
||||
Object.keys(layouts.value).forEach((monitor) => {
|
||||
const leftItems = layouts.value[monitor].left;
|
||||
const rightItems = layouts.value[monitor].right;
|
||||
const middleItems = layouts.value[monitor].middle;
|
||||
|
||||
itemsInLayout.push(...leftItems);
|
||||
itemsInLayout.push(...middleItems);
|
||||
itemsInLayout.push(...rightItems);
|
||||
});
|
||||
|
||||
return [...new Set(itemsInLayout)];
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns substitute icon || name || fallback icon
|
||||
*/
|
||||
export function icon(name: string | null, fallback = icons.missing): string {
|
||||
const validateSubstitute = (name: string): name is SubstituteKeys => name in substitutes;
|
||||
|
||||
if (!name) return fallback || '';
|
||||
|
||||
if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name;
|
||||
|
||||
let icon: string = name;
|
||||
|
||||
if (validateSubstitute(name)) {
|
||||
icon = substitutes[name];
|
||||
}
|
||||
|
||||
if (Utils.lookUpIcon(icon)) return icon;
|
||||
|
||||
print(`no icon substitute "${icon}" for "${name}", fallback: "${fallback}"`);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns execAsync(["bash", "-c", cmd])
|
||||
*/
|
||||
export async function bash(strings: TemplateStringsArray | string, ...values: unknown[]): Promise<string> {
|
||||
const cmd =
|
||||
typeof strings === 'string' ? strings : strings.flatMap((str, i) => str + `${values[i] ?? ''}`).join('');
|
||||
|
||||
return Utils.execAsync(['bash', '-c', cmd]).catch((err) => {
|
||||
console.error(cmd, err);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns execAsync(cmd)
|
||||
*/
|
||||
export async function sh(cmd: string | string[]): Promise<string> {
|
||||
return Utils.execAsync(cmd).catch((err) => {
|
||||
console.error(typeof cmd === 'string' ? cmd : cmd.join(' '), err);
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
export function forMonitors(widget: (monitor: number) => Gtk.Window): Window[] {
|
||||
const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
|
||||
return range(n, 0).flatMap(widget);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns [start...length]
|
||||
*/
|
||||
export function range(length: number, start = 1): number[] {
|
||||
return Array.from({ length }, (_, i) => i + start);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if all of the `bins` are found
|
||||
*/
|
||||
export function dependencies(...bins: string[]): boolean {
|
||||
const missing = bins.filter((bin) =>
|
||||
Utils.exec({
|
||||
cmd: `which ${bin}`,
|
||||
out: () => false,
|
||||
err: () => true,
|
||||
}),
|
||||
);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.warn(Error(`missing dependencies: ${missing.join(', ')}`));
|
||||
Notify({
|
||||
summary: 'Dependencies not found!',
|
||||
body: `The following dependencies are missing: ${missing.join(', ')}`,
|
||||
iconName: icons.ui.warning,
|
||||
timeout: 7000,
|
||||
});
|
||||
}
|
||||
|
||||
return missing.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* run app detached
|
||||
*/
|
||||
export function launchApp(app: Application): void {
|
||||
const exe = app.executable
|
||||
.split(/\s+/)
|
||||
.filter((str) => !str.startsWith('%') && !str.startsWith('@'))
|
||||
.join(' ');
|
||||
|
||||
bash(`${exe} &`);
|
||||
app.frequency += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* to use with drag and drop
|
||||
*/
|
||||
export function createSurfaceFromWidget(widget: Gtk.Widget): GdkPixbuf.Pixbuf {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const cairo = imports.gi.cairo as any;
|
||||
const alloc = widget.get_allocation();
|
||||
const surface = new cairo.ImageSurface(cairo.Format.ARGB32, alloc.width, alloc.height);
|
||||
const cr = new cairo.Context(surface);
|
||||
cr.setSourceRGBA(255, 255, 255, 0);
|
||||
cr.rectangle(0, 0, alloc.width, alloc.height);
|
||||
cr.fill();
|
||||
widget.draw(cr);
|
||||
return surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the provided filepath is a valid image
|
||||
*/
|
||||
export const isAnImage = (imgFilePath: string): boolean => {
|
||||
try {
|
||||
GdkPixbuf.Pixbuf.new_from_file(imgFilePath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const Notify = (notifPayload: NotificationArgs): void => {
|
||||
let command = 'notify-send';
|
||||
command += ` "${notifPayload.summary} "`;
|
||||
if (notifPayload.body) command += ` "${notifPayload.body}" `;
|
||||
if (notifPayload.appName) command += ` -a "${notifPayload.appName}"`;
|
||||
if (notifPayload.iconName) command += ` -i "${notifPayload.iconName}"`;
|
||||
if (notifPayload.urgency) command += ` -u "${notifPayload.urgency}"`;
|
||||
if (notifPayload.timeout !== undefined) command += ` -t ${notifPayload.timeout}`;
|
||||
if (notifPayload.category) command += ` -c "${notifPayload.category}"`;
|
||||
if (notifPayload.transient) command += ` -e`;
|
||||
if (notifPayload.id !== undefined) command += ` -r ${notifPayload.id}`;
|
||||
|
||||
Utils.execAsync(command);
|
||||
};
|
||||
|
||||
export const getPosition = (pos: NotificationAnchor | OSDAnchor): ('top' | 'bottom' | 'left' | 'right')[] => {
|
||||
const positionMap: { [key: string]: ('top' | 'bottom' | 'left' | 'right')[] } = {
|
||||
top: ['top'],
|
||||
'top right': ['top', 'right'],
|
||||
'top left': ['top', 'left'],
|
||||
bottom: ['bottom'],
|
||||
'bottom right': ['bottom', 'right'],
|
||||
'bottom left': ['bottom', 'left'],
|
||||
right: ['right'],
|
||||
left: ['left'],
|
||||
};
|
||||
|
||||
return positionMap[pos] || ['top'];
|
||||
};
|
||||
export const isValidGjsColor = (color: string): boolean => {
|
||||
const colorLower = color.toLowerCase().trim();
|
||||
|
||||
if (namedColors.has(colorLower)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hexColorRegex = /^#(?:[a-fA-F0-9]{3,4}|[a-fA-F0-9]{6,8})$/;
|
||||
|
||||
const rgbRegex = /^rgb\(\s*(\d{1,3}%?\s*,\s*){2}\d{1,3}%?\s*\)$/;
|
||||
const rgbaRegex = /^rgba\(\s*(\d{1,3}%?\s*,\s*){3}(0|1|0?\.\d+)\s*\)$/;
|
||||
|
||||
if (hexColorRegex.test(color)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rgbRegex.test(colorLower) || rgbaRegex.test(colorLower)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const capitalizeFirstLetter = (str: string): string => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
};
|
||||
|
||||
export function getDistroIcon(): string {
|
||||
const icon = distroIcons.find(([id]) => id === distro.id);
|
||||
return icon ? icon[1] : ''; // default icon if not found
|
||||
}
|
||||
|
||||
export const warnOnLowBattery = (): void => {
|
||||
battery.connect('notify::percent', () => {
|
||||
const { lowBatteryThreshold, lowBatteryNotification, lowBatteryNotificationText, lowBatteryNotificationTitle } =
|
||||
options.menus.power;
|
||||
if (!lowBatteryNotification.value || battery.charging) return;
|
||||
const lowThreshold = lowBatteryThreshold.value;
|
||||
|
||||
if (battery.percent === lowThreshold || battery.percent === lowThreshold / 2) {
|
||||
Notify({
|
||||
summary: lowBatteryNotificationTitle.value.replace('/$POWER_LEVEL/g', battery.percent.toString()),
|
||||
body: lowBatteryNotificationText.value.replace('/$POWER_LEVEL/g', battery.percent.toString()),
|
||||
iconName: icons.ui.warning,
|
||||
urgency: 'critical',
|
||||
timeout: 7000,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import GLib from 'gi://GLib';
|
||||
import { DateTime } from 'types/@girs/glib-2.0/glib-2.0.cjs';
|
||||
|
||||
export const clock = Variable(GLib.DateTime.new_now_local(), {
|
||||
poll: [1000, (): DateTime => GLib.DateTime.new_now_local()],
|
||||
});
|
||||
|
||||
export const uptime = Variable(0, {
|
||||
poll: [60_000, 'cat /proc/uptime', (line): number => Number.parseInt(line.split('.')[0]) / 60],
|
||||
});
|
||||
|
||||
export const distro = {
|
||||
id: GLib.get_os_info('ID'),
|
||||
logo: GLib.get_os_info('LOGO'),
|
||||
};
|
||||
68
main.ts
68
main.ts
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
makepkg -si
|
||||
|
||||
echo "Cleaning up build files..."
|
||||
rm -rf -- pkg src *.pkg.tar.* *.tar.gz
|
||||
|
||||
echo "Build and installation completed successfully. All generated files have been cleaned up."
|
||||
64
meson.build
Normal file
64
meson.build
Normal file
@@ -0,0 +1,64 @@
|
||||
project('hyprpanel')
|
||||
|
||||
bindir = get_option('prefix') / get_option('bindir')
|
||||
datadir = get_option('prefix') / get_option('datadir') / 'hyprpanel'
|
||||
|
||||
ags = find_program('ags', required: true)
|
||||
find_program('gjs', required: true)
|
||||
|
||||
src_file_list_process = run_command(
|
||||
'find',
|
||||
'src',
|
||||
'-type', 'f',
|
||||
'(',
|
||||
'-name', '*.ts',
|
||||
'-o',
|
||||
'-name', '*.tsx',
|
||||
'-o',
|
||||
'-name', '*.scss',
|
||||
')',
|
||||
)
|
||||
|
||||
if src_file_list_process.returncode() != 0
|
||||
error('Failed to find source files.')
|
||||
endif
|
||||
|
||||
src_file_list = src_file_list_process.stdout().split('\n')
|
||||
|
||||
all_sources = []
|
||||
|
||||
foreach file : src_file_list
|
||||
file_stripped = file.strip()
|
||||
if file_stripped != ''
|
||||
all_sources += meson.project_source_root() / file_stripped
|
||||
endif
|
||||
endforeach
|
||||
|
||||
custom_target(
|
||||
'hyprpanel_bundle',
|
||||
input: all_sources,
|
||||
command: [
|
||||
ags,
|
||||
'bundle',
|
||||
meson.project_source_root() / 'app.ts',
|
||||
'@OUTPUT@',
|
||||
'--src', meson.project_source_root(),
|
||||
],
|
||||
output: 'hyprpanel.js',
|
||||
install: true,
|
||||
install_dir: datadir,
|
||||
)
|
||||
|
||||
configure_file(
|
||||
input: 'scripts/hyprpanel_launcher.sh.in',
|
||||
output: 'hyprpanel',
|
||||
configuration: {'DATADIR': datadir},
|
||||
install: true,
|
||||
install_dir: bindir,
|
||||
install_mode: 'rwxr-xr-x',
|
||||
)
|
||||
|
||||
install_subdir('scripts', install_dir: datadir)
|
||||
install_subdir('themes', install_dir: datadir)
|
||||
install_subdir('assets', install_dir: datadir)
|
||||
install_subdir('src/scss', install_dir: datadir / 'src')
|
||||
@@ -1,301 +0,0 @@
|
||||
const hyprland = await Service.import('hyprland');
|
||||
|
||||
import {
|
||||
Menu,
|
||||
Workspaces,
|
||||
ClientTitle,
|
||||
Media,
|
||||
Notifications,
|
||||
Volume,
|
||||
Network,
|
||||
Bluetooth,
|
||||
BatteryLabel,
|
||||
Clock,
|
||||
SysTray,
|
||||
|
||||
// Custom Modules
|
||||
Ram,
|
||||
Cpu,
|
||||
CpuTemp,
|
||||
Storage,
|
||||
Netstat,
|
||||
KbInput,
|
||||
Updates,
|
||||
Submap,
|
||||
Weather,
|
||||
Power,
|
||||
Hyprsunset,
|
||||
Hypridle,
|
||||
} from './Exports';
|
||||
|
||||
import { BarItemBox as WidgetContainer } from '../shared/barItemBox.js';
|
||||
import options from 'options';
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
|
||||
|
||||
import './SideEffects';
|
||||
import { BarLayout, BarLayouts, WindowLayer } from 'lib/types/options.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import Window from 'types/widgets/window.js';
|
||||
|
||||
const { layouts } = options.bar;
|
||||
const { location } = options.theme.bar;
|
||||
const { location: borderLocation } = options.theme.bar.border;
|
||||
|
||||
const getLayoutForMonitor = (monitor: number, layouts: BarLayouts): BarLayout => {
|
||||
const matchingKey = Object.keys(layouts).find((key) => key === monitor.toString());
|
||||
const wildcard = Object.keys(layouts).find((key) => key === '*');
|
||||
|
||||
if (matchingKey) {
|
||||
return layouts[matchingKey];
|
||||
}
|
||||
|
||||
if (wildcard) {
|
||||
return layouts[wildcard];
|
||||
}
|
||||
|
||||
return {
|
||||
left: ['dashboard', 'workspaces', 'windowtitle'],
|
||||
middle: ['media'],
|
||||
right: ['volume', 'network', 'bluetooth', 'battery', 'systray', 'clock', 'notifications'],
|
||||
};
|
||||
};
|
||||
|
||||
const isLayoutEmpty = (layout: BarLayout): boolean => {
|
||||
const isLeftSectionEmpty = !Array.isArray(layout.left) || layout.left.length === 0;
|
||||
const isRightSectionEmpty = !Array.isArray(layout.right) || layout.right.length === 0;
|
||||
const isMiddleSectionEmpty = !Array.isArray(layout.middle) || layout.middle.length === 0;
|
||||
|
||||
return isLeftSectionEmpty && isRightSectionEmpty && isMiddleSectionEmpty;
|
||||
};
|
||||
|
||||
const widget = {
|
||||
battery: (): Button<Child, Attribute> => WidgetContainer(BatteryLabel()),
|
||||
dashboard: (): Button<Child, Attribute> => WidgetContainer(Menu()),
|
||||
workspaces: (monitor: number): Button<Child, Attribute> => WidgetContainer(Workspaces(monitor)),
|
||||
windowtitle: (): Button<Child, Attribute> => WidgetContainer(ClientTitle()),
|
||||
media: (): Button<Child, Attribute> => WidgetContainer(Media()),
|
||||
notifications: (): Button<Child, Attribute> => WidgetContainer(Notifications()),
|
||||
volume: (): Button<Child, Attribute> => WidgetContainer(Volume()),
|
||||
network: (): Button<Child, Attribute> => WidgetContainer(Network()),
|
||||
bluetooth: (): Button<Child, Attribute> => WidgetContainer(Bluetooth()),
|
||||
clock: (): Button<Child, Attribute> => WidgetContainer(Clock()),
|
||||
systray: (): Button<Child, Attribute> => WidgetContainer(SysTray()),
|
||||
ram: (): Button<Child, Attribute> => WidgetContainer(Ram()),
|
||||
cpu: (): Button<Child, Attribute> => WidgetContainer(Cpu()),
|
||||
cputemp: (): Button<Child, Attribute> => WidgetContainer(CpuTemp()),
|
||||
storage: (): Button<Child, Attribute> => WidgetContainer(Storage()),
|
||||
netstat: (): Button<Child, Attribute> => WidgetContainer(Netstat()),
|
||||
kbinput: (): Button<Child, Attribute> => WidgetContainer(KbInput()),
|
||||
updates: (): Button<Child, Attribute> => WidgetContainer(Updates()),
|
||||
submap: (): Button<Child, Attribute> => WidgetContainer(Submap()),
|
||||
weather: (): Button<Child, Attribute> => WidgetContainer(Weather()),
|
||||
power: (): Button<Child, Attribute> => WidgetContainer(Power()),
|
||||
hyprsunset: (): Button<Child, Attribute> => WidgetContainer(Hyprsunset()),
|
||||
hypridle: (): Button<Child, Attribute> => WidgetContainer(Hypridle()),
|
||||
};
|
||||
|
||||
type GdkMonitors = {
|
||||
[key: string]: {
|
||||
key: string;
|
||||
model: string;
|
||||
used: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
function getGdkMonitors(): GdkMonitors {
|
||||
const display = Gdk.Display.get_default();
|
||||
|
||||
if (display === null) {
|
||||
console.error('Failed to get Gdk display.');
|
||||
return {};
|
||||
}
|
||||
|
||||
const numGdkMonitors = display.get_n_monitors();
|
||||
const gdkMonitors: GdkMonitors = {};
|
||||
|
||||
for (let i = 0; i < numGdkMonitors; i++) {
|
||||
const curMonitor = display.get_monitor(i);
|
||||
|
||||
if (curMonitor === null) {
|
||||
console.warn(`Monitor at index ${i} is null.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const model = curMonitor.get_model() || '';
|
||||
const geometry = curMonitor.get_geometry();
|
||||
const scaleFactor = curMonitor.get_scale_factor();
|
||||
|
||||
const key = `${model}_${geometry.width}x${geometry.height}_${scaleFactor}`;
|
||||
gdkMonitors[i] = { key, model, used: false };
|
||||
}
|
||||
|
||||
return gdkMonitors;
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Some more funky stuff being done by GDK.
|
||||
* We render windows/bar based on the monitor ID. So if you have 3 monitors, then your
|
||||
* monitor IDs will be [0, 1, 2]. Hyprland will NEVER change what ID belongs to what monitor.
|
||||
*
|
||||
* So if hyprland determines id 0 = DP-1, even after you unplug, shut off or restart your monitor,
|
||||
* the id 0 will ALWAYS be DP-1.
|
||||
*
|
||||
* However, GDK (the righteous genius that it is) will change the order of ID anytime your monitor
|
||||
* setup is changed. So if you unplug your monitor and plug it back it, it now becomes the last id.
|
||||
* So if DP-1 was id 0 and you unplugged it, it will reconfigure to id 2. This sucks because now
|
||||
* there's a mismtach between what GDK determines the monitor is at id 2 and what Hyprland determines
|
||||
* is at id 2.
|
||||
*
|
||||
* So for that reason, we need to redirect the input `monitor` that the Bar module takes in, to the
|
||||
* proper Hyprland monitor. So when monitor id 0 comes in, we need to find what the id of that monitor
|
||||
* is being determined as by Hyprland so the bars show up on the right monitors.
|
||||
*
|
||||
* Since GTK3 doesn't contain connection names and only monitor models, we have to make the best guess
|
||||
* in the case that there are multiple models in the same resolution with the same scale. We find the
|
||||
* 'right' monitor by checking if the model matches along with the resolution and scale. If monitor at
|
||||
* ID 0 for GDK is being reported as 'MSI MAG271CQR' we find the same model in the Hyprland monitor list
|
||||
* and check if the resolution and scaling is the same... if it is then we determine it's a match.
|
||||
*
|
||||
* The edge-case that we just can't handle is if you have the same monitors in the same resolution at the same
|
||||
* scale. So if you've got 2 'MSI MAG271CQR' monitors at 2560x1440 at scale 1, then we just match the first
|
||||
* monitor in the list as the first match and then the second 'MSI MAG271CQR' as a match in the 2nd iteration.
|
||||
* You may have the bar showing up on the wrong one in this case because we don't know what the connector id
|
||||
* is of either of these monitors (DP-1, DP-2) which are unique values - as these are only in GTK4.
|
||||
*
|
||||
* Keep in mind though, this is ONLY an issue if you change your monitor setup by plugging in a new one, restarting
|
||||
* an existing one or shutting it off.
|
||||
*
|
||||
* If your monitors aren't changed in the current session you're in then none of this safeguarding is relevant.
|
||||
*
|
||||
* Fun stuff really... :facepalm:
|
||||
*/
|
||||
|
||||
const gdkMonitorIdToHyprlandId = (monitor: number, usedHyprlandMonitors: Set<number>): number => {
|
||||
const gdkMonitors = getGdkMonitors();
|
||||
|
||||
if (Object.keys(gdkMonitors).length === 0) {
|
||||
console.error('No GDK monitors were found.');
|
||||
return monitor;
|
||||
}
|
||||
|
||||
// Get the GDK monitor for the given monitor index
|
||||
const gdkMonitor = gdkMonitors[monitor];
|
||||
|
||||
// First pass: Strict matching including the monitor index (i.e., hypMon.id === monitor + resolution+scale criteria)
|
||||
const directMatch = hyprland.monitors.find((hypMon) => {
|
||||
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
|
||||
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id) && hypMon.id === monitor;
|
||||
});
|
||||
|
||||
if (directMatch) {
|
||||
usedHyprlandMonitors.add(directMatch.id);
|
||||
return directMatch.id;
|
||||
}
|
||||
|
||||
// Second pass: Relaxed matching without considering the monitor index
|
||||
const hyprlandMonitor = hyprland.monitors.find((hypMon) => {
|
||||
const hyprlandKey = `${hypMon.model}_${hypMon.width}x${hypMon.height}_${hypMon.scale}`;
|
||||
return gdkMonitor.key.startsWith(hyprlandKey) && !usedHyprlandMonitors.has(hypMon.id);
|
||||
});
|
||||
|
||||
if (hyprlandMonitor) {
|
||||
usedHyprlandMonitors.add(hyprlandMonitor.id);
|
||||
return hyprlandMonitor.id;
|
||||
}
|
||||
|
||||
// Fallback: Find the first available monitor ID that hasn't been used
|
||||
const fallbackMonitor = hyprland.monitors.find((hypMon) => !usedHyprlandMonitors.has(hypMon.id));
|
||||
|
||||
if (fallbackMonitor) {
|
||||
usedHyprlandMonitors.add(fallbackMonitor.id);
|
||||
return fallbackMonitor.id;
|
||||
}
|
||||
|
||||
// Ensure we return a valid monitor ID that actually exists
|
||||
for (let i = 0; i < hyprland.monitors.length; i++) {
|
||||
if (!usedHyprlandMonitors.has(i)) {
|
||||
usedHyprlandMonitors.add(i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// As a last resort, return the original monitor index if no unique monitor can be found
|
||||
console.warn(`Returning original monitor index as a last resort: ${monitor}`);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
export const Bar = (() => {
|
||||
const usedHyprlandMonitors = new Set<number>();
|
||||
|
||||
return (monitor: number): Window<Child, Attribute> => {
|
||||
const hyprlandMonitor = gdkMonitorIdToHyprlandId(monitor, usedHyprlandMonitors);
|
||||
|
||||
return Widget.Window({
|
||||
name: `bar-${hyprlandMonitor}`,
|
||||
class_name: 'bar',
|
||||
monitor,
|
||||
visible: layouts.bind('value').as(() => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
|
||||
return !isLayoutEmpty(foundLayout);
|
||||
}),
|
||||
anchor: location.bind('value').as((ln) => [ln, 'left', 'right']),
|
||||
exclusivity: 'exclusive',
|
||||
layer: Utils.merge(
|
||||
[options.theme.bar.layer.bind('value'), options.tear.bind('value')],
|
||||
(barLayer: WindowLayer, tear: boolean) => {
|
||||
if (tear && barLayer === 'overlay') {
|
||||
return 'top';
|
||||
}
|
||||
return barLayer;
|
||||
},
|
||||
),
|
||||
child: Widget.Box({
|
||||
class_name: 'bar-panel-container',
|
||||
child: Widget.CenterBox({
|
||||
class_name: borderLocation
|
||||
.bind('value')
|
||||
.as((brdrLcn) => (brdrLcn !== 'none' ? 'bar-panel withBorder' : 'bar-panel')),
|
||||
css: 'padding: 1px',
|
||||
startWidget: Widget.Box({
|
||||
class_name: 'box-left',
|
||||
hexpand: true,
|
||||
setup: (self) => {
|
||||
self.hook(layouts, (self) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
|
||||
self.children = foundLayout.left
|
||||
.filter((mod) => Object.keys(widget).includes(mod))
|
||||
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
|
||||
});
|
||||
},
|
||||
}),
|
||||
centerWidget: Widget.Box({
|
||||
class_name: 'box-center',
|
||||
hpack: 'center',
|
||||
setup: (self) => {
|
||||
self.hook(layouts, (self) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
|
||||
self.children = foundLayout.middle
|
||||
.filter((mod) => Object.keys(widget).includes(mod))
|
||||
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
|
||||
});
|
||||
},
|
||||
}),
|
||||
endWidget: Widget.Box({
|
||||
class_name: 'box-right',
|
||||
hpack: 'end',
|
||||
setup: (self) => {
|
||||
self.hook(layouts, (self) => {
|
||||
const foundLayout = getLayoutForMonitor(hyprlandMonitor, layouts.value);
|
||||
self.children = foundLayout.right
|
||||
.filter((mod) => Object.keys(widget).includes(mod))
|
||||
.map((w) => widget[w](hyprlandMonitor) as Button<Gtk.Widget, unknown>);
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
})();
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
const battery = await Service.import('battery');
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
import { openMenu } from '../utils.js';
|
||||
import options from 'options';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
|
||||
|
||||
const { label: show_label, rightClick, middleClick, scrollUp, scrollDown, hideLabelWhenFull } = options.bar.battery;
|
||||
|
||||
const BatteryLabel = (): BarBoxChild => {
|
||||
const isVis = Variable(battery.available);
|
||||
|
||||
const batIcon = Utils.merge(
|
||||
[battery.bind('percent'), battery.bind('charging'), battery.bind('charged')],
|
||||
(batPercent: number, batCharging, batCharged) => {
|
||||
if (batCharged) return `battery-level-100-charged-symbolic`;
|
||||
else return `battery-level-${Math.floor(batPercent / 10) * 10}${batCharging ? '-charging' : ''}-symbolic`;
|
||||
},
|
||||
);
|
||||
|
||||
battery.connect('changed', ({ available }) => {
|
||||
isVis.value = available;
|
||||
});
|
||||
|
||||
const formatTime = (seconds: number): Record<string, number> => {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return { hours, minutes };
|
||||
};
|
||||
|
||||
const generateTooltip = (timeSeconds: number, isCharging: boolean, isCharged: boolean): string => {
|
||||
if (isCharged) {
|
||||
return 'Full';
|
||||
}
|
||||
|
||||
const { hours, minutes } = formatTime(timeSeconds);
|
||||
if (isCharging) {
|
||||
return `Time to full: ${hours} h ${minutes} min`;
|
||||
} else {
|
||||
return `Time to empty: ${hours} h ${minutes} min`;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
component: Widget.Box({
|
||||
className: Utils.merge(
|
||||
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `battery-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
),
|
||||
visible: battery.bind('available'),
|
||||
tooltip_text: battery.bind('time_remaining').as((t) => t.toString()),
|
||||
children: Utils.merge(
|
||||
[
|
||||
battery.bind('available'),
|
||||
show_label.bind('value'),
|
||||
battery.bind('charged'),
|
||||
hideLabelWhenFull.bind('value'),
|
||||
],
|
||||
(batAvail, showLabel, isCharged, hideWhenFull) => {
|
||||
if (batAvail && showLabel) {
|
||||
return [
|
||||
Widget.Icon({
|
||||
class_name: 'bar-button-icon battery',
|
||||
icon: batIcon,
|
||||
}),
|
||||
...(hideWhenFull && isCharged
|
||||
? []
|
||||
: [
|
||||
Widget.Label({
|
||||
class_name: 'bar-button-label battery',
|
||||
label: battery.bind('percent').as((p) => `${Math.floor(p)}%`),
|
||||
}),
|
||||
]),
|
||||
];
|
||||
} else if (batAvail && !showLabel) {
|
||||
return [
|
||||
Widget.Icon({
|
||||
class_name: 'bar-button-icon battery',
|
||||
icon: batIcon,
|
||||
}),
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
),
|
||||
setup: (self) => {
|
||||
self.hook(battery, () => {
|
||||
if (battery.available) {
|
||||
self.tooltip_text = generateTooltip(battery.time_remaining, battery.charging, battery.charged);
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
isVis,
|
||||
boxClass: 'battery',
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
onPrimaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'energymenu');
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { BatteryLabel };
|
||||
@@ -1,74 +0,0 @@
|
||||
const bluetooth = await Service.import('bluetooth');
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
import options from 'options';
|
||||
import { openMenu } from '../utils.js';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
|
||||
|
||||
const { label, rightClick, middleClick, scrollDown, scrollUp } = options.bar.bluetooth;
|
||||
|
||||
const Bluetooth = (): BarBoxChild => {
|
||||
const btIcon = Widget.Label({
|
||||
label: bluetooth.bind('enabled').as((v) => (v ? '' : '')),
|
||||
class_name: 'bar-button-icon bluetooth txt-icon bar',
|
||||
});
|
||||
|
||||
const btText = Widget.Label({
|
||||
label: Utils.merge([bluetooth.bind('enabled'), bluetooth.bind('connected_devices')], (btEnabled, btDevices) => {
|
||||
return btEnabled && btDevices.length ? ` Connected (${btDevices.length})` : btEnabled ? 'On' : 'Off';
|
||||
}),
|
||||
class_name: 'bar-button-label bluetooth',
|
||||
});
|
||||
|
||||
return {
|
||||
component: Widget.Box({
|
||||
className: Utils.merge(
|
||||
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `bluetooth-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
),
|
||||
children: options.bar.bluetooth.label.bind('value').as((showLabel) => {
|
||||
if (showLabel) {
|
||||
return [btIcon, btText];
|
||||
}
|
||||
return [btIcon];
|
||||
}),
|
||||
}),
|
||||
isVisible: true,
|
||||
boxClass: 'bluetooth',
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'bluetoothmenu');
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Bluetooth };
|
||||
@@ -1,83 +0,0 @@
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
import GLib from 'gi://GLib';
|
||||
import { openMenu } from '../utils.js';
|
||||
import options from 'options';
|
||||
import { DateTime } from 'types/@girs/glib-2.0/glib-2.0.cjs';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
|
||||
|
||||
const { format, icon, showIcon, showTime, rightClick, middleClick, scrollUp, scrollDown } = options.bar.clock;
|
||||
const { style } = options.theme.bar.buttons;
|
||||
|
||||
const date = Variable(GLib.DateTime.new_now_local(), {
|
||||
poll: [1000, (): DateTime => GLib.DateTime.new_now_local()],
|
||||
});
|
||||
const time = Utils.derive([date, format], (c, f) => c.format(f) || '');
|
||||
|
||||
const Clock = (): BarBoxChild => {
|
||||
const clockTime = Widget.Label({
|
||||
class_name: 'bar-button-label clock bar',
|
||||
label: time.bind(),
|
||||
});
|
||||
|
||||
const clockIcon = Widget.Label({
|
||||
label: icon.bind('value'),
|
||||
class_name: 'bar-button-icon clock txt-icon bar',
|
||||
});
|
||||
|
||||
return {
|
||||
component: Widget.Box({
|
||||
className: Utils.merge(
|
||||
[style.bind('value'), showIcon.bind('value'), showTime.bind('value')],
|
||||
(btnStyle, shwIcn, shwLbl) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
|
||||
return `clock-container ${styleMap[btnStyle]} ${!shwLbl ? 'no-label' : ''} ${!shwIcn ? 'no-icon' : ''}`;
|
||||
},
|
||||
),
|
||||
children: Utils.merge([showIcon.bind('value'), showTime.bind('value')], (shIcn, shTm) => {
|
||||
if (shIcn && !shTm) {
|
||||
return [clockIcon];
|
||||
} else if (shTm && !shIcn) {
|
||||
return [clockTime];
|
||||
}
|
||||
|
||||
return [clockIcon, clockTime];
|
||||
}),
|
||||
}),
|
||||
isVisible: true,
|
||||
boxClass: 'clock',
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'calendarmenu');
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Clock };
|
||||
@@ -1,89 +0,0 @@
|
||||
import { MediaTags } from 'lib/types/audio.js';
|
||||
import { Opt } from 'lib/option';
|
||||
import { Variable } from 'types/variable';
|
||||
import { MprisPlayer } from 'types/service/mpris';
|
||||
|
||||
const getIconForPlayer = (playerName: string): string => {
|
||||
const windowTitleMap = [
|
||||
['Firefox', ''],
|
||||
['Microsoft Edge', ''],
|
||||
['Discord', ''],
|
||||
['Plex', ''],
|
||||
['Spotify', ''],
|
||||
['Vlc', ''],
|
||||
['Mpv', ''],
|
||||
['Rhythmbox', ''],
|
||||
['Google Chrome', ''],
|
||||
['Brave Browser', ''],
|
||||
['Chromium', ''],
|
||||
['Opera', ''],
|
||||
['Vivaldi', ''],
|
||||
['Waterfox', ''],
|
||||
['Thorium', ''],
|
||||
['Zen Browser', ''],
|
||||
['Floorp', ''],
|
||||
['(.*)', ''],
|
||||
];
|
||||
|
||||
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0], 'i').test(playerName));
|
||||
|
||||
return foundMatch ? foundMatch[1] : '';
|
||||
};
|
||||
|
||||
const isValidMediaTag = (tag: unknown): tag is keyof MediaTags => {
|
||||
if (typeof tag !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mediaTagKeys = ['title', 'artists', 'artist', 'album', 'name', 'identity'] as const;
|
||||
return (mediaTagKeys as readonly string[]).includes(tag);
|
||||
};
|
||||
|
||||
export const generateMediaLabel = (
|
||||
truncation_size: Opt<number>,
|
||||
show_label: Opt<boolean>,
|
||||
format: Opt<string>,
|
||||
songIcon: Variable<string>,
|
||||
activePlayer: Variable<MprisPlayer>,
|
||||
): string => {
|
||||
if (activePlayer.value && show_label.value) {
|
||||
const { track_title, identity, track_artists, track_album, name } = activePlayer.value;
|
||||
songIcon.value = getIconForPlayer(identity);
|
||||
|
||||
const mediaTags: MediaTags = {
|
||||
title: track_title,
|
||||
artists: track_artists.join(', '),
|
||||
artist: track_artists[0] || '',
|
||||
album: track_album,
|
||||
name: name,
|
||||
identity: identity,
|
||||
};
|
||||
|
||||
const mediaFormat = format.getValue();
|
||||
|
||||
const truncatedLabel = mediaFormat.replace(
|
||||
/{(title|artists|artist|album|name|identity)(:[^}]*)?}/g,
|
||||
(_, p1: string | undefined, p2: string | undefined) => {
|
||||
if (!isValidMediaTag(p1)) {
|
||||
return '';
|
||||
}
|
||||
const value = p1 !== undefined ? mediaTags[p1] : '';
|
||||
const suffix = p2?.length ? p2.slice(1) : '';
|
||||
return value ? value + suffix : '';
|
||||
},
|
||||
);
|
||||
|
||||
const maxLabelSize = truncation_size.value;
|
||||
|
||||
let mediaLabel = truncatedLabel;
|
||||
|
||||
if (maxLabelSize > 0 && truncatedLabel.length > maxLabelSize) {
|
||||
mediaLabel = `${truncatedLabel.substring(0, maxLabelSize)}...`;
|
||||
}
|
||||
|
||||
return mediaLabel.length ? mediaLabel : 'Media';
|
||||
} else {
|
||||
songIcon.value = getIconForPlayer(activePlayer.value?.identity || '');
|
||||
return `Media`;
|
||||
}
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
const mpris = await Service.import('mpris');
|
||||
import { openMenu } from '../utils.js';
|
||||
import options from 'options';
|
||||
import { getCurrentPlayer } from 'lib/shared/media.js';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import { runAsyncCommand } from 'customModules/utils.js';
|
||||
import { generateMediaLabel } from './helpers.js';
|
||||
|
||||
const { truncation, truncation_size, show_label, show_active_only, rightClick, middleClick, format } =
|
||||
options.bar.media;
|
||||
|
||||
const Media = (): BarBoxChild => {
|
||||
const activePlayer = Variable(mpris.players[0]);
|
||||
const isVis = Variable(!show_active_only.value);
|
||||
|
||||
show_active_only.connect('changed', () => {
|
||||
isVis.value = !show_active_only.value || mpris.players.length > 0;
|
||||
});
|
||||
|
||||
mpris.connect('changed', () => {
|
||||
const curPlayer = getCurrentPlayer(activePlayer.value);
|
||||
activePlayer.value = curPlayer;
|
||||
isVis.value = !show_active_only.value || mpris.players.length > 0;
|
||||
});
|
||||
|
||||
const songIcon = Variable('');
|
||||
|
||||
const mediaLabel = Utils.watch('Media', [mpris, truncation, truncation_size, show_label, format], () => {
|
||||
return generateMediaLabel(truncation_size, show_label, format, songIcon, activePlayer);
|
||||
});
|
||||
|
||||
return {
|
||||
component: Widget.Box({
|
||||
visible: false,
|
||||
child: Widget.Box({
|
||||
className: Utils.merge(
|
||||
[options.theme.bar.buttons.style.bind('value'), show_label.bind('value')],
|
||||
(style) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `media-container ${styleMap[style]}`;
|
||||
},
|
||||
),
|
||||
child: Widget.Box({
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'bar-button-icon media txt-icon bar',
|
||||
label: songIcon.bind('value').as((v) => v || ''),
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'bar-button-label media',
|
||||
label: mediaLabel,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
isVis,
|
||||
boxClass: 'media',
|
||||
props: {
|
||||
on_scroll_up: () => activePlayer.value?.next(),
|
||||
on_scroll_down: () => activePlayer.value?.previous(),
|
||||
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'mediamenu');
|
||||
},
|
||||
onSecondaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
},
|
||||
onMiddleClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Media };
|
||||
@@ -1,59 +0,0 @@
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { openMenu } from '../utils.js';
|
||||
import { getDistroIcon } from 'lib/utils.js';
|
||||
|
||||
const { rightClick, middleClick, scrollUp, scrollDown, autoDetectIcon, icon } = options.bar.launcher;
|
||||
|
||||
const Menu = (): BarBoxChild => {
|
||||
return {
|
||||
component: Widget.Box({
|
||||
className: Utils.merge([options.theme.bar.buttons.style.bind('value')], (style) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `dashboard ${styleMap[style]}`;
|
||||
}),
|
||||
child: Widget.Label({
|
||||
class_name: 'bar-menu_label bar-button_icon txt-icon bar',
|
||||
label: Utils.merge([autoDetectIcon.bind('value'), icon.bind('value')], (autoDetect, icon): string => {
|
||||
return autoDetect ? getDistroIcon() : icon;
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
isVisible: true,
|
||||
boxClass: 'dashboard',
|
||||
props: {
|
||||
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'dashboardmenu');
|
||||
},
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Menu };
|
||||
@@ -1,122 +0,0 @@
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
const network = await Service.import('network');
|
||||
import options from 'options';
|
||||
import { openMenu } from '../utils.js';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
|
||||
import { Wifi } from 'types/service/network.js';
|
||||
|
||||
const formatFrequency = (frequency: number): string => {
|
||||
return `${(frequency / 1000).toFixed(2)}MHz`;
|
||||
};
|
||||
|
||||
const formatWifiInfo = (wifi: Wifi): string => {
|
||||
const netSsid = wifi.ssid === '' ? 'None' : wifi.ssid;
|
||||
const wifiStrength = wifi.strength >= 0 ? wifi.strength : '--';
|
||||
const wifiFreq = wifi.frequency >= 0 ? formatFrequency(wifi.frequency) : '--';
|
||||
|
||||
return `Network: ${netSsid} \nSignal Strength: ${wifiStrength}% \nFrequency: ${wifiFreq}`;
|
||||
};
|
||||
|
||||
const {
|
||||
label: networkLabel,
|
||||
truncation,
|
||||
truncation_size,
|
||||
rightClick,
|
||||
middleClick,
|
||||
scrollDown,
|
||||
scrollUp,
|
||||
showWifiInfo,
|
||||
} = options.bar.network;
|
||||
|
||||
const Network = (): BarBoxChild => {
|
||||
return {
|
||||
component: Widget.Box({
|
||||
vpack: 'fill',
|
||||
vexpand: true,
|
||||
className: Utils.merge(
|
||||
[options.theme.bar.buttons.style.bind('value'), networkLabel.bind('value')],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `network-container ${styleMap[style]}${!showLabel ? ' no-label' : ''}`;
|
||||
},
|
||||
),
|
||||
children: [
|
||||
Widget.Icon({
|
||||
class_name: 'bar-button-icon network-icon',
|
||||
icon: Utils.merge(
|
||||
[network.bind('primary'), network.bind('wifi'), network.bind('wired')],
|
||||
(pmry, wfi, wrd) => {
|
||||
if (pmry === 'wired') {
|
||||
return wrd.icon_name;
|
||||
}
|
||||
return wfi.icon_name;
|
||||
},
|
||||
),
|
||||
}),
|
||||
Widget.Box({
|
||||
child: Utils.merge(
|
||||
[
|
||||
network.bind('primary'),
|
||||
network.bind('wifi'),
|
||||
networkLabel.bind('value'),
|
||||
truncation.bind('value'),
|
||||
truncation_size.bind('value'),
|
||||
showWifiInfo.bind('value'),
|
||||
],
|
||||
(pmry, wfi, showLbl, trunc, tSize, showWfiInfo) => {
|
||||
if (!showLbl) {
|
||||
return Widget.Box();
|
||||
}
|
||||
if (pmry === 'wired') {
|
||||
return Widget.Label({
|
||||
class_name: 'bar-button-label network-label',
|
||||
label: 'Wired'.substring(0, tSize),
|
||||
});
|
||||
}
|
||||
return Widget.Label({
|
||||
class_name: 'bar-button-label network-label',
|
||||
label: wfi.ssid ? `${trunc ? wfi.ssid.substring(0, tSize) : wfi.ssid}` : '--',
|
||||
tooltipText: showWfiInfo ? formatWifiInfo(wfi) : '',
|
||||
});
|
||||
},
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
isVisible: true,
|
||||
boxClass: 'network',
|
||||
props: {
|
||||
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'networkmenu');
|
||||
},
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Network };
|
||||
@@ -1,94 +0,0 @@
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
import { openMenu } from '../utils.js';
|
||||
import options from 'options';
|
||||
import { filterNotifications } from 'lib/shared/notifications.js';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
|
||||
|
||||
const { show_total, rightClick, middleClick, scrollUp, scrollDown, hideCountWhenZero } = options.bar.notifications;
|
||||
const { ignore } = options.notifications;
|
||||
|
||||
const notifs = await Service.import('notifications');
|
||||
|
||||
export const Notifications = (): BarBoxChild => {
|
||||
return {
|
||||
component: Widget.Box({
|
||||
hpack: 'start',
|
||||
className: Utils.merge(
|
||||
[options.theme.bar.buttons.style.bind('value'), show_total.bind('value')],
|
||||
(style, showTotal) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `notifications-container ${styleMap[style]} ${!showTotal ? 'no-label' : ''}`;
|
||||
},
|
||||
),
|
||||
child: Widget.Box({
|
||||
hpack: 'start',
|
||||
class_name: 'bar-notifications',
|
||||
children: Utils.merge(
|
||||
[
|
||||
notifs.bind('notifications'),
|
||||
notifs.bind('dnd'),
|
||||
show_total.bind('value'),
|
||||
ignore.bind('value'),
|
||||
hideCountWhenZero.bind('value'),
|
||||
],
|
||||
(notif, dnd, showTotal, ignoredNotifs, hideCountForZero) => {
|
||||
const filteredNotifications = filterNotifications(notif, ignoredNotifs);
|
||||
|
||||
const notifIcon = Widget.Label({
|
||||
hpack: 'center',
|
||||
class_name: 'bar-button-icon notifications txt-icon bar',
|
||||
label: dnd ? '' : filteredNotifications.length > 0 ? '' : '',
|
||||
});
|
||||
|
||||
const notifLabel = Widget.Label({
|
||||
hpack: 'center',
|
||||
class_name: 'bar-button-label notifications',
|
||||
label: filteredNotifications.length.toString(),
|
||||
});
|
||||
|
||||
if (showTotal) {
|
||||
if (hideCountForZero && filteredNotifications.length === 0) {
|
||||
return [notifIcon];
|
||||
}
|
||||
return [notifIcon, notifLabel];
|
||||
}
|
||||
return [notifIcon];
|
||||
},
|
||||
),
|
||||
}),
|
||||
}),
|
||||
isVisible: true,
|
||||
boxClass: 'notifications',
|
||||
props: {
|
||||
on_primary_click: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'notificationsmenu');
|
||||
},
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,107 +0,0 @@
|
||||
import Gdk from 'gi://Gdk?version=3.0';
|
||||
const audio = await Service.import('audio');
|
||||
import { openMenu } from '../utils.js';
|
||||
import options from 'options';
|
||||
import { Binding } from 'lib/utils.js';
|
||||
import { VolumeIcons } from 'lib/types/volume.js';
|
||||
import { BarBoxChild } from 'lib/types/bar.js';
|
||||
import { Bind } from 'lib/types/variable.js';
|
||||
import Button from 'types/widgets/button.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils.js';
|
||||
|
||||
const { rightClick, middleClick, scrollUp, scrollDown } = options.bar.volume;
|
||||
|
||||
const Volume = (): BarBoxChild => {
|
||||
const icons: VolumeIcons = {
|
||||
101: '',
|
||||
66: '',
|
||||
34: '',
|
||||
1: '',
|
||||
0: '',
|
||||
};
|
||||
|
||||
const getIcon = (): Bind => {
|
||||
const icon: Binding<number> = Utils.merge(
|
||||
[audio.speaker.bind('is_muted'), audio.speaker.bind('volume')],
|
||||
(isMuted, vol) => {
|
||||
if (isMuted) return 0;
|
||||
|
||||
const foundVol = [101, 66, 34, 1, 0].find((threshold) => threshold <= vol * 100);
|
||||
|
||||
if (foundVol !== undefined) {
|
||||
return foundVol;
|
||||
}
|
||||
|
||||
return 101;
|
||||
},
|
||||
);
|
||||
|
||||
return icon.as((i: number) => (i !== undefined ? icons[i] : icons[101]));
|
||||
};
|
||||
|
||||
const volIcn = Widget.Label({
|
||||
label: getIcon(),
|
||||
class_name: 'bar-button-icon volume txt-icon bar',
|
||||
});
|
||||
|
||||
const volPct = Widget.Label({
|
||||
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
|
||||
class_name: 'bar-button-label volume',
|
||||
});
|
||||
|
||||
return {
|
||||
component: Widget.Box({
|
||||
vexpand: true,
|
||||
tooltip_text: Utils.merge(
|
||||
[audio.speaker.bind('description'), getIcon()],
|
||||
(desc, icon) => ` ${icon} ${desc}`,
|
||||
),
|
||||
className: Utils.merge(
|
||||
[options.theme.bar.buttons.style.bind('value'), options.bar.volume.label.bind('value')],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `volume-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
),
|
||||
children: options.bar.volume.label.bind('value').as((showLabel) => {
|
||||
if (showLabel) {
|
||||
return [volIcn, volPct];
|
||||
}
|
||||
return [volIcn];
|
||||
}),
|
||||
}),
|
||||
isVisible: true,
|
||||
boxClass: 'volume',
|
||||
props: {
|
||||
onPrimaryClick: (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
openMenu(clicked, event, 'audiomenu');
|
||||
},
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { Volume };
|
||||
@@ -1,242 +0,0 @@
|
||||
const hyprland = await Service.import('hyprland');
|
||||
import { runAsyncCommand, throttledScrollHandler } from 'customModules/utils';
|
||||
import { BarBoxChild } from 'lib/types/bar';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
import { capitalizeFirstLetter } from 'lib/utils';
|
||||
import options from 'options';
|
||||
import Gdk from 'types/@girs/gdk-3.0/gdk-3.0';
|
||||
import { ActiveClient } from 'types/service/hyprland';
|
||||
import Button from 'types/widgets/button';
|
||||
import Label from 'types/widgets/label';
|
||||
|
||||
const { leftClick, rightClick, middleClick, scrollDown, scrollUp } = options.bar.windowtitle;
|
||||
|
||||
const filterTitle = (windowtitle: ActiveClient): Record<string, string> => {
|
||||
const windowTitleMap = [
|
||||
// user provided values
|
||||
...options.bar.windowtitle.title_map.value,
|
||||
// Original Entries
|
||||
['kitty', '', 'Kitty Terminal'],
|
||||
['firefox', '', 'Firefox'],
|
||||
['microsoft-edge', '', 'Edge'],
|
||||
['discord', '', 'Discord'],
|
||||
['vesktop', '', 'Vesktop'],
|
||||
['org.kde.dolphin', '', 'Dolphin'],
|
||||
['plex', '', 'Plex'],
|
||||
['steam', '', 'Steam'],
|
||||
['spotify', '', 'Spotify'],
|
||||
['ristretto', '', 'Ristretto'],
|
||||
['obsidian', '', 'Obsidian'],
|
||||
|
||||
// Browsers
|
||||
['google-chrome', '', 'Google Chrome'],
|
||||
['brave-browser', '', 'Brave Browser'],
|
||||
['chromium', '', 'Chromium'],
|
||||
['opera', '', 'Opera'],
|
||||
['vivaldi', '', 'Vivaldi'],
|
||||
['waterfox', '', 'Waterfox'],
|
||||
['thorium', '', 'Waterfox'],
|
||||
['tor-browser', '', 'Tor Browser'],
|
||||
['floorp', '', 'Floorp'],
|
||||
|
||||
// Terminals
|
||||
['gnome-terminal', '', 'GNOME Terminal'],
|
||||
['konsole', '', 'Konsole'],
|
||||
['alacritty', '', 'Alacritty'],
|
||||
['wezterm', '', 'Wezterm'],
|
||||
['foot', '', 'Foot Terminal'],
|
||||
['tilix', '', 'Tilix'],
|
||||
['xterm', '', 'XTerm'],
|
||||
['urxvt', '', 'URxvt'],
|
||||
['st', '', 'st Terminal'],
|
||||
|
||||
// Development Tools
|
||||
['code', '', 'Visual Studio Code'],
|
||||
['vscode', '', 'VS Code'],
|
||||
['sublime-text', '', 'Sublime Text'],
|
||||
['atom', '', 'Atom'],
|
||||
['android-studio', '', 'Android Studio'],
|
||||
['intellij-idea', '', 'IntelliJ IDEA'],
|
||||
['pycharm', '', 'PyCharm'],
|
||||
['webstorm', '', 'WebStorm'],
|
||||
['phpstorm', '', 'PhpStorm'],
|
||||
['eclipse', '', 'Eclipse'],
|
||||
['netbeans', '', 'NetBeans'],
|
||||
['docker', '', 'Docker'],
|
||||
['vim', '', 'Vim'],
|
||||
['neovim', '', 'Neovim'],
|
||||
['neovide', '', 'Neovide'],
|
||||
['emacs', '', 'Emacs'],
|
||||
|
||||
// Communication Tools
|
||||
['slack', '', 'Slack'],
|
||||
['telegram-desktop', '', 'Telegram'],
|
||||
['org.telegram.desktop', '', 'Telegram'],
|
||||
['whatsapp', '', 'WhatsApp'],
|
||||
['teams', '', 'Microsoft Teams'],
|
||||
['skype', '', 'Skype'],
|
||||
['thunderbird', '', 'Thunderbird'],
|
||||
|
||||
// File Managers
|
||||
['nautilus', '', 'Files (Nautilus)'],
|
||||
['thunar', '', 'Thunar'],
|
||||
['pcmanfm', '', 'PCManFM'],
|
||||
['nemo', '', 'Nemo'],
|
||||
['ranger', '', 'Ranger'],
|
||||
['doublecmd', '', 'Double Commander'],
|
||||
['krusader', '', 'Krusader'],
|
||||
|
||||
// Media Players
|
||||
['vlc', '', 'VLC Media Player'],
|
||||
['mpv', '', 'MPV'],
|
||||
['rhythmbox', '', 'Rhythmbox'],
|
||||
|
||||
// Graphics Tools
|
||||
['gimp', '', 'GIMP'],
|
||||
['inkscape', '', 'Inkscape'],
|
||||
['krita', '', 'Krita'],
|
||||
['blender', '', 'Blender'],
|
||||
|
||||
// Video Editing
|
||||
['kdenlive', '', 'Kdenlive'],
|
||||
|
||||
// Games and Gaming Platforms
|
||||
['lutris', '', 'Lutris'],
|
||||
['heroic', '', 'Heroic Games Launcher'],
|
||||
['minecraft', '', 'Minecraft'],
|
||||
['csgo', '', 'CS:GO'],
|
||||
['dota2', '', 'Dota 2'],
|
||||
|
||||
// Office and Productivity
|
||||
['evernote', '', 'Evernote'],
|
||||
['sioyek', '', 'Sioyek'],
|
||||
|
||||
// Cloud Services and Sync
|
||||
['dropbox', '', 'Dropbox'],
|
||||
|
||||
// Desktop
|
||||
['^$', '', 'Desktop'],
|
||||
|
||||
// Fallback icon
|
||||
['(.+)', '', `${capitalizeFirstLetter(windowtitle.class)}`],
|
||||
];
|
||||
|
||||
const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(windowtitle.class.toLowerCase()));
|
||||
|
||||
// return the default icon if no match is found or
|
||||
// if the array element matched is not of size 3
|
||||
if (!foundMatch || foundMatch.length !== 3) {
|
||||
return {
|
||||
icon: windowTitleMap[windowTitleMap.length - 1][1],
|
||||
label: windowTitleMap[windowTitleMap.length - 1][2],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
icon: foundMatch[1],
|
||||
label: foundMatch[2],
|
||||
};
|
||||
};
|
||||
|
||||
const getTitle = (client: ActiveClient, useCustomTitle: boolean, useClassName: boolean): string => {
|
||||
if (useCustomTitle) return filterTitle(client).label;
|
||||
if (useClassName) return client.class;
|
||||
|
||||
const title = client.title;
|
||||
// If the title is empty or only filled with spaces, fallback to the class name
|
||||
if (title.length === 0 || title.match(/^ *$/)) {
|
||||
return client.class;
|
||||
}
|
||||
return title;
|
||||
};
|
||||
|
||||
const truncateTitle = (title: string, max_size: number): string => {
|
||||
if (max_size > 0 && title.length > max_size) {
|
||||
return title.substring(0, max_size).trim() + '...';
|
||||
}
|
||||
return title;
|
||||
};
|
||||
|
||||
const ClientTitle = (): BarBoxChild => {
|
||||
const { custom_title, class_name, label, icon, truncation, truncation_size } = options.bar.windowtitle;
|
||||
|
||||
return {
|
||||
component: Widget.Box({
|
||||
className: Utils.merge(
|
||||
[options.theme.bar.buttons.style.bind('value'), label.bind('value')],
|
||||
(style, showLabel) => {
|
||||
const styleMap = {
|
||||
default: 'style1',
|
||||
split: 'style2',
|
||||
wave: 'style3',
|
||||
wave2: 'style3',
|
||||
};
|
||||
return `windowtitle-container ${styleMap[style]} ${!showLabel ? 'no-label' : ''}`;
|
||||
},
|
||||
),
|
||||
children: Utils.merge(
|
||||
[
|
||||
hyprland.active.bind('client'),
|
||||
custom_title.bind('value'),
|
||||
class_name.bind('value'),
|
||||
label.bind('value'),
|
||||
icon.bind('value'),
|
||||
truncation.bind('value'),
|
||||
truncation_size.bind('value'),
|
||||
],
|
||||
(client, useCustomTitle, useClassName, showLabel, showIcon, truncate, truncationSize) => {
|
||||
const children: Label<Child>[] = [];
|
||||
if (showIcon) {
|
||||
children.push(
|
||||
Widget.Label({
|
||||
class_name: 'bar-button-icon windowtitle txt-icon bar',
|
||||
label: filterTitle(client).icon,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (showLabel) {
|
||||
children.push(
|
||||
Widget.Label({
|
||||
class_name: `bar-button-label windowtitle ${showIcon ? '' : 'no-icon'}`,
|
||||
label: truncateTitle(
|
||||
getTitle(client, useCustomTitle, useClassName),
|
||||
truncate ? truncationSize : -1,
|
||||
),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
},
|
||||
),
|
||||
}),
|
||||
isVisible: true,
|
||||
boxClass: 'windowtitle',
|
||||
props: {
|
||||
setup: (self: Button<Child, Attribute>): void => {
|
||||
self.hook(options.bar.scrollSpeed, () => {
|
||||
const throttledHandler = throttledScrollHandler(options.bar.scrollSpeed.value);
|
||||
|
||||
self.on_primary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(leftClick.value, { clicked, event });
|
||||
};
|
||||
self.on_secondary_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(rightClick.value, { clicked, event });
|
||||
};
|
||||
self.on_middle_click = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
runAsyncCommand(middleClick.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_up = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollUp.value, { clicked, event });
|
||||
};
|
||||
self.on_scroll_down = (clicked: Button<Child, Attribute>, event: Gdk.Event): void => {
|
||||
throttledHandler(scrollDown.value, { clicked, event });
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export { ClientTitle };
|
||||
@@ -1,162 +0,0 @@
|
||||
const hyprland = await Service.import('hyprland');
|
||||
|
||||
import { MonitorMap, WorkspaceMap, WorkspaceRule } from 'lib/types/workspace';
|
||||
import options from 'options';
|
||||
import { Variable } from 'types/variable';
|
||||
|
||||
const { workspaces, reverse_scroll, ignored } = options.bar.workspaces;
|
||||
|
||||
export const getWorkspacesForMonitor = (curWs: number, wsRules: WorkspaceMap, monitor: number): boolean => {
|
||||
if (!wsRules || !Object.keys(wsRules).length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const monitorMap: MonitorMap = {};
|
||||
const workspaceMonitorList = hyprland?.workspaces?.map((m) => ({ id: m.monitorID, name: m.monitor }));
|
||||
const monitors = [
|
||||
...new Map([...workspaceMonitorList, ...hyprland.monitors].map((item) => [item.id, item])).values(),
|
||||
];
|
||||
|
||||
monitors.forEach((m) => (monitorMap[m.id] = m.name));
|
||||
|
||||
const currentMonitorName = monitorMap[monitor];
|
||||
const monitorWSRules = wsRules[currentMonitorName];
|
||||
|
||||
if (monitorWSRules === undefined) {
|
||||
return true;
|
||||
}
|
||||
return monitorWSRules.includes(curWs);
|
||||
};
|
||||
|
||||
export const getWorkspaceRules = (): WorkspaceMap => {
|
||||
try {
|
||||
const rules = Utils.exec('hyprctl workspacerules -j');
|
||||
|
||||
const workspaceRules: WorkspaceMap = {};
|
||||
|
||||
JSON.parse(rules).forEach((rule: WorkspaceRule) => {
|
||||
const workspaceNum = parseInt(rule.workspaceString, 10);
|
||||
if (isNaN(workspaceNum)) {
|
||||
return;
|
||||
}
|
||||
if (Object.hasOwnProperty.call(workspaceRules, rule.monitor)) {
|
||||
workspaceRules[rule.monitor].push(workspaceNum);
|
||||
} else {
|
||||
workspaceRules[rule.monitor] = [workspaceNum];
|
||||
}
|
||||
});
|
||||
|
||||
return workspaceRules;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const getCurrentMonitorWorkspaces = (monitor: number): number[] => {
|
||||
if (hyprland.monitors.length === 1) {
|
||||
return Array.from({ length: workspaces.value }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const monitorWorkspaces = getWorkspaceRules();
|
||||
const monitorMap: MonitorMap = {};
|
||||
hyprland.monitors.forEach((m) => (monitorMap[m.id] = m.name));
|
||||
|
||||
const currentMonitorName = monitorMap[monitor];
|
||||
|
||||
return monitorWorkspaces[currentMonitorName];
|
||||
};
|
||||
|
||||
type ThrottledScrollHandlers = {
|
||||
throttledScrollUp: () => void;
|
||||
throttledScrollDown: () => void;
|
||||
};
|
||||
|
||||
export const isWorkspaceIgnored = (ignoredWorkspaces: Variable<string>, workspaceNumber: number): boolean => {
|
||||
if (ignoredWorkspaces.value === '') return false;
|
||||
|
||||
const ignoredWsRegex = new RegExp(ignoredWorkspaces.value);
|
||||
|
||||
return ignoredWsRegex.test(workspaceNumber.toString());
|
||||
};
|
||||
|
||||
const navigateWorkspace = (
|
||||
direction: 'next' | 'prev',
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean,
|
||||
ignoredWorkspaces: Variable<string>,
|
||||
): void => {
|
||||
const workspacesList = activeWorkspaces
|
||||
? hyprland.workspaces.filter((ws) => hyprland.active.monitor.id === ws.monitorID).map((ws) => ws.id)
|
||||
: currentMonitorWorkspaces.value || Array.from({ length: workspaces.value }, (_, i) => i + 1);
|
||||
|
||||
if (workspacesList.length === 0) return;
|
||||
|
||||
const currentIndex = workspacesList.indexOf(hyprland.active.workspace.id);
|
||||
const step = direction === 'next' ? 1 : -1;
|
||||
let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length;
|
||||
let attempts = 0;
|
||||
|
||||
while (attempts < workspacesList.length) {
|
||||
const targetWS = workspacesList[newIndex];
|
||||
if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) {
|
||||
hyprland.messageAsync(`dispatch workspace ${targetWS}`);
|
||||
return;
|
||||
}
|
||||
newIndex = (newIndex + step + workspacesList.length) % workspacesList.length;
|
||||
attempts++;
|
||||
}
|
||||
};
|
||||
|
||||
export const goToNextWS = (
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean,
|
||||
ignoredWorkspaces: Variable<string>,
|
||||
): void => {
|
||||
navigateWorkspace('next', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
|
||||
};
|
||||
|
||||
export const goToPrevWS = (
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean,
|
||||
ignoredWorkspaces: Variable<string>,
|
||||
): void => {
|
||||
navigateWorkspace('prev', currentMonitorWorkspaces, activeWorkspaces, ignoredWorkspaces);
|
||||
};
|
||||
|
||||
export function throttle<T extends (...args: unknown[]) => void>(func: T, limit: number): T {
|
||||
let inThrottle: boolean;
|
||||
return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => {
|
||||
inThrottle = false;
|
||||
}, limit);
|
||||
}
|
||||
} as T;
|
||||
}
|
||||
|
||||
export const createThrottledScrollHandlers = (
|
||||
scrollSpeed: number,
|
||||
currentMonitorWorkspaces: Variable<number[]>,
|
||||
activeWorkspaces: boolean = false,
|
||||
): ThrottledScrollHandlers => {
|
||||
const throttledScrollUp = throttle(() => {
|
||||
if (reverse_scroll.value) {
|
||||
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
} else {
|
||||
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
const throttledScrollDown = throttle(() => {
|
||||
if (reverse_scroll.value) {
|
||||
goToNextWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
} else {
|
||||
goToPrevWS(currentMonitorWorkspaces, activeWorkspaces, ignored);
|
||||
}
|
||||
}, 200 / scrollSpeed);
|
||||
|
||||
return { throttledScrollUp, throttledScrollDown };
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,199 +0,0 @@
|
||||
import { defaultApplicationIcons } from 'lib/constants/workspaces';
|
||||
import type { ClientAttributes, AppIconOptions, WorkspaceIconMap } from 'lib/types/workspace';
|
||||
import { isValidGjsColor } from 'lib/utils';
|
||||
import options from 'options';
|
||||
import { Monitor } from 'types/service/hyprland';
|
||||
|
||||
const hyprland = await Service.import('hyprland');
|
||||
|
||||
const { monochrome, background } = options.theme.bar.buttons;
|
||||
const { background: wsBackground, active } = options.theme.bar.buttons.workspaces;
|
||||
|
||||
const { showWsIcons, showAllActive, numbered_active_indicator: activeIndicator } = options.bar.workspaces;
|
||||
|
||||
const isWorkspaceActiveOnMonitor = (monitor: number, monitors: Monitor[], i: number): boolean => {
|
||||
return showAllActive.value && monitors[monitor]?.activeWorkspace?.id === i;
|
||||
};
|
||||
|
||||
const getWsIcon = (wsIconMap: WorkspaceIconMap, i: number): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
|
||||
if (!iconEntry) {
|
||||
return `${i}`;
|
||||
}
|
||||
|
||||
const hasIcon = typeof iconEntry === 'object' && 'icon' in iconEntry && iconEntry.icon !== '';
|
||||
|
||||
if (typeof iconEntry === 'string' && iconEntry !== '') {
|
||||
return iconEntry;
|
||||
}
|
||||
|
||||
if (hasIcon) {
|
||||
return iconEntry.icon;
|
||||
}
|
||||
|
||||
return `${i}`;
|
||||
};
|
||||
|
||||
export const getWsColor = (
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
monitors: Monitor[],
|
||||
): string => {
|
||||
const iconEntry = wsIconMap[i];
|
||||
const hasColor = typeof iconEntry === 'object' && 'color' in iconEntry && isValidGjsColor(iconEntry.color);
|
||||
if (!iconEntry) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
showWsIcons.value &&
|
||||
smartHighlight &&
|
||||
activeIndicator.value === 'highlight' &&
|
||||
(hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i))
|
||||
) {
|
||||
const iconColor = monochrome.value ? background : wsBackground;
|
||||
const iconBackground = hasColor && isValidGjsColor(iconEntry.color) ? iconEntry.color : active.value;
|
||||
const colorCss = `color: ${iconColor};`;
|
||||
const backgroundCss = `background: ${iconBackground};`;
|
||||
|
||||
return colorCss + backgroundCss;
|
||||
}
|
||||
|
||||
if (hasColor && isValidGjsColor(iconEntry.color)) {
|
||||
return `color: ${iconEntry.color}; border-bottom-color: ${iconEntry.color};`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getAppIcon = (
|
||||
workspaceIndex: number,
|
||||
removeDuplicateIcons: boolean,
|
||||
{ iconMap: userDefinedIconMap, defaultIcon, emptyIcon }: AppIconOptions,
|
||||
): string => {
|
||||
// append the default icons so user defined icons take precedence
|
||||
const iconMap = { ...userDefinedIconMap, ...defaultApplicationIcons };
|
||||
|
||||
// detect the clients attributes on the current workspace
|
||||
const clients: ReadonlyArray<ClientAttributes> = hyprland.clients
|
||||
.filter((c) => c.workspace.id === workspaceIndex)
|
||||
.map((c) => [c.class, c.title]);
|
||||
|
||||
if (!clients.length) {
|
||||
return emptyIcon;
|
||||
}
|
||||
|
||||
// map the client attributes to icons
|
||||
let icons = clients
|
||||
.map(([clientClass, clientTitle]) => {
|
||||
const maybeIcon = Object.entries(iconMap).find(([matcher]) => {
|
||||
// non-valid Regex construction could result in a syntax error
|
||||
try {
|
||||
if (matcher.startsWith('class:')) {
|
||||
const re = matcher.substring(6);
|
||||
return new RegExp(re).test(clientClass);
|
||||
}
|
||||
|
||||
if (matcher.startsWith('title:')) {
|
||||
const re = matcher.substring(6);
|
||||
|
||||
return new RegExp(re).test(clientTitle);
|
||||
}
|
||||
|
||||
return new RegExp(matcher, 'i').test(clientClass);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!maybeIcon) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return maybeIcon.at(1);
|
||||
})
|
||||
.filter((x) => x);
|
||||
|
||||
// remove duplicate icons
|
||||
if (removeDuplicateIcons) {
|
||||
icons = [...new Set(icons)];
|
||||
}
|
||||
|
||||
if (icons.length) {
|
||||
return icons.join(' ');
|
||||
}
|
||||
|
||||
return defaultIcon;
|
||||
};
|
||||
|
||||
export const renderClassnames = (
|
||||
showIcons: boolean,
|
||||
showNumbered: boolean,
|
||||
numberedActiveIndicator: string,
|
||||
showWsIcons: boolean,
|
||||
smartHighlight: boolean,
|
||||
monitor: number,
|
||||
monitors: Monitor[],
|
||||
i: number,
|
||||
): string => {
|
||||
if (showIcons) {
|
||||
return 'workspace-icon txt-icon bar';
|
||||
}
|
||||
|
||||
if (showNumbered || showWsIcons) {
|
||||
const numActiveInd =
|
||||
hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)
|
||||
? numberedActiveIndicator
|
||||
: '';
|
||||
|
||||
const wsIconClass = showWsIcons ? 'txt-icon' : '';
|
||||
const smartHighlightClass = smartHighlight ? 'smart-highlight' : '';
|
||||
|
||||
const className = `workspace-number can_${numberedActiveIndicator} ${numActiveInd} ${wsIconClass} ${smartHighlightClass}`;
|
||||
|
||||
return className.trim();
|
||||
}
|
||||
|
||||
return 'default';
|
||||
};
|
||||
|
||||
export const renderLabel = (
|
||||
showIcons: boolean,
|
||||
available: string,
|
||||
active: string,
|
||||
occupied: string,
|
||||
showAppIcons: boolean,
|
||||
appIcons: string,
|
||||
workspaceMask: boolean,
|
||||
showWsIcons: boolean,
|
||||
wsIconMap: WorkspaceIconMap,
|
||||
i: number,
|
||||
index: number,
|
||||
monitor: number,
|
||||
monitors: Monitor[],
|
||||
): string => {
|
||||
if (showAppIcons) {
|
||||
return appIcons;
|
||||
}
|
||||
|
||||
if (showIcons) {
|
||||
if (hyprland.active.workspace.id === i || isWorkspaceActiveOnMonitor(monitor, monitors, i)) {
|
||||
return active;
|
||||
}
|
||||
if ((hyprland.getWorkspace(i)?.windows || 0) > 0) {
|
||||
return occupied;
|
||||
}
|
||||
if (monitor !== -1) {
|
||||
return available;
|
||||
}
|
||||
}
|
||||
|
||||
if (showWsIcons) {
|
||||
return getWsIcon(wsIconMap, i);
|
||||
}
|
||||
|
||||
return workspaceMask ? `${index + 1}` : `${i}`;
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -1,75 +0,0 @@
|
||||
const audio = await Service.import('audio');
|
||||
import { getIcon } from '../utils.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const renderActiveInput = (): Box<Child, Attribute>[] => {
|
||||
return [
|
||||
Widget.Box({
|
||||
class_name: 'menu-slider-container input',
|
||||
children: [
|
||||
Widget.Button({
|
||||
vexpand: false,
|
||||
vpack: 'end',
|
||||
setup: (self) => {
|
||||
const updateClass = (): void => {
|
||||
const mic = audio.microphone;
|
||||
const className = `menu-active-button input ${mic.is_muted ? 'muted' : ''}`;
|
||||
self.class_name = className;
|
||||
};
|
||||
|
||||
self.hook(audio.microphone, updateClass, 'notify::is-muted');
|
||||
},
|
||||
on_primary_click: () => (audio.microphone.is_muted = !audio.microphone.is_muted),
|
||||
child: Widget.Icon({
|
||||
class_name: 'menu-active-icon input',
|
||||
setup: (self) => {
|
||||
const updateIcon = (): void => {
|
||||
const isMicMuted =
|
||||
audio.microphone.is_muted !== null ? audio.microphone.is_muted : true;
|
||||
|
||||
if (audio.microphone.volume > 0) {
|
||||
self.icon = getIcon(audio.microphone.volume, isMicMuted)['mic'];
|
||||
} else {
|
||||
self.icon = getIcon(100, true)['mic'];
|
||||
}
|
||||
};
|
||||
self.hook(audio.microphone, updateIcon, 'notify::volume');
|
||||
self.hook(audio.microphone, updateIcon, 'notify::is-muted');
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-active input',
|
||||
hpack: 'start',
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: audio.bind('microphone').as((v) => {
|
||||
return v.description === null ? 'No input device found...' : v.description;
|
||||
}),
|
||||
}),
|
||||
Widget.Slider({
|
||||
value: audio.microphone.bind('volume').as((v) => v),
|
||||
class_name: 'menu-active-slider menu-slider inputs',
|
||||
draw_value: false,
|
||||
hexpand: true,
|
||||
min: 0,
|
||||
max: 1,
|
||||
onChange: ({ value }) => (audio.microphone.volume = value),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'menu-active-percentage input',
|
||||
vpack: 'end',
|
||||
label: audio.microphone.bind('volume').as((v) => `${Math.round(v * 100)}%`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
export { renderActiveInput };
|
||||
@@ -1,76 +0,0 @@
|
||||
const audio = await Service.import('audio');
|
||||
import { getIcon } from '../utils.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options';
|
||||
|
||||
const { raiseMaximumVolume } = options.menus.volume;
|
||||
|
||||
const renderActivePlayback = (): Box<Child, Attribute>[] => {
|
||||
return [
|
||||
Widget.Box({
|
||||
class_name: 'menu-slider-container playback',
|
||||
children: [
|
||||
Widget.Button({
|
||||
vexpand: false,
|
||||
vpack: 'end',
|
||||
setup: (self) => {
|
||||
const updateClass = (): void => {
|
||||
const spkr = audio.speaker;
|
||||
const className = `menu-active-button playback ${spkr.is_muted ? 'muted' : ''}`;
|
||||
self.class_name = className;
|
||||
};
|
||||
|
||||
self.hook(audio.speaker, updateClass, 'notify::is-muted');
|
||||
},
|
||||
on_primary_click: () => (audio.speaker.is_muted = !audio.speaker.is_muted),
|
||||
child: Widget.Icon({
|
||||
class_name: 'menu-active-icon playback',
|
||||
setup: (self) => {
|
||||
const updateIcon = (): void => {
|
||||
const isSpeakerMuted = audio.speaker.is_muted !== null ? audio.speaker.is_muted : true;
|
||||
self.icon = getIcon(audio.speaker.volume, isSpeakerMuted)['spkr'];
|
||||
};
|
||||
self.hook(audio.speaker, updateIcon, 'notify::volume');
|
||||
self.hook(audio.speaker, updateIcon, 'notify::is-muted');
|
||||
},
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'menu-active playback',
|
||||
hpack: 'start',
|
||||
truncate: 'end',
|
||||
expand: true,
|
||||
wrap: true,
|
||||
label: audio.bind('speaker').as((v) => v.description || ''),
|
||||
}),
|
||||
Widget.Slider({
|
||||
value: audio['speaker'].bind('volume'),
|
||||
class_name: 'menu-active-slider menu-slider playback',
|
||||
draw_value: false,
|
||||
hexpand: true,
|
||||
min: 0,
|
||||
max: 1,
|
||||
onChange: ({ value }) => (audio.speaker.volume = value),
|
||||
setup: (self) => {
|
||||
self.hook(raiseMaximumVolume, () => {
|
||||
self.max = raiseMaximumVolume.value ? 1.5 : 1;
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Label({
|
||||
vpack: 'end',
|
||||
class_name: 'menu-active-percentage playback',
|
||||
label: audio.speaker.bind('volume').as((v) => `${Math.round(v * 100)}%`),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
export { renderActivePlayback };
|
||||
@@ -1,41 +0,0 @@
|
||||
import { renderActiveInput } from './SelectedInput.js';
|
||||
import { renderActivePlayback } from './SelectedPlayback.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
|
||||
const activeDevices = (): Box<Child, Attribute> => {
|
||||
return Widget.Box({
|
||||
class_name: 'menu-section-container volume',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-label-container volume selected',
|
||||
hpack: 'fill',
|
||||
child: Widget.Label({
|
||||
class_name: 'menu-label audio volume',
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
label: 'Volume',
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-items-section selected',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'menu-active-container playback',
|
||||
vertical: true,
|
||||
children: renderActivePlayback(),
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'menu-active-container input',
|
||||
vertical: true,
|
||||
children: renderActiveInput(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { activeDevices };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1,25 +0,0 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { activeDevices } from './active/index.js';
|
||||
import { availableDevices } from './available/index.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'audiomenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items audio',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container audio',
|
||||
children: [activeDevices(), availableDevices()],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,188 +0,0 @@
|
||||
import { Bluetooth, BluetoothDevice } from 'types/service/bluetooth.js';
|
||||
import Box from 'types/widgets/box.js';
|
||||
import { connectedControls } from './connectedControls.js';
|
||||
import { getBluetoothIcon } from '../utils.js';
|
||||
import Gtk from 'types/@girs/gtk-3.0/gtk-3.0.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options';
|
||||
import Label from 'types/widgets/label.js';
|
||||
|
||||
const { showBattery, batteryIcon, batteryState } = options.menus.bluetooth;
|
||||
|
||||
const devices = (bluetooth: Bluetooth, self: Box<Gtk.Widget, unknown>): Box<Child, Attribute> => {
|
||||
return self.hook(bluetooth, () => {
|
||||
if (!bluetooth.enabled) {
|
||||
return (self.child = Widget.Box({
|
||||
class_name: 'bluetooth-items',
|
||||
vertical: true,
|
||||
expand: true,
|
||||
vpack: 'center',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'bluetooth-disabled dim',
|
||||
hexpand: true,
|
||||
label: 'Bluetooth is disabled',
|
||||
}),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
const availableDevices = bluetooth.devices
|
||||
.filter((btDev) => btDev.name !== null)
|
||||
.sort((a, b) => {
|
||||
if (a.connected || a.paired) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b.connected || b.paired) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return b.name - a.name;
|
||||
});
|
||||
|
||||
const conDevNames = availableDevices.filter((d) => d.connected || d.paired).map((d) => d.address);
|
||||
|
||||
if (!availableDevices.length) {
|
||||
return (self.child = Widget.Box({
|
||||
class_name: 'bluetooth-items',
|
||||
vertical: true,
|
||||
expand: true,
|
||||
vpack: 'center',
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'no-bluetooth-devices dim',
|
||||
hexpand: true,
|
||||
label: 'No devices currently found',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'search-bluetooth-label dim',
|
||||
hexpand: true,
|
||||
label: "Press '' to search",
|
||||
}),
|
||||
],
|
||||
}));
|
||||
}
|
||||
|
||||
const getConnectionStatusLabel = (device: BluetoothDevice): Label<Attribute> => {
|
||||
return Widget.Label({
|
||||
hpack: 'start',
|
||||
class_name: 'connection-status dim',
|
||||
label: device.connected ? 'Connected' : 'Paired',
|
||||
});
|
||||
};
|
||||
|
||||
const getBatteryInfo = (device: BluetoothDevice, batIcon: string): Gtk.Widget[] => {
|
||||
if (typeof device.battery_percentage === 'number' && device.battery_percentage >= 0) {
|
||||
return [
|
||||
Widget.Separator({
|
||||
class_name: 'menu-separator bluetooth-battery',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'connection-status txt-icon',
|
||||
label: `${batIcon}`,
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'connection-status battery',
|
||||
label: `${device.battery_percentage}%`,
|
||||
}),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
return (self.child = Widget.Box({
|
||||
vertical: true,
|
||||
children: availableDevices.map((device) => {
|
||||
return Widget.Box({
|
||||
children: [
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
class_name: `bluetooth-element-item ${device}`,
|
||||
on_primary_click: () => {
|
||||
if (!conDevNames.includes(device.address)) device.setConnection(true);
|
||||
},
|
||||
child: Widget.Box({
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
hpack: 'start',
|
||||
class_name: 'menu-button-container',
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'start',
|
||||
class_name: `menu-button-icon bluetooth ${conDevNames.includes(device.address) ? 'active' : ''} txt-icon`,
|
||||
label: getBluetoothIcon(`${device['icon_name']}-symbolic`),
|
||||
}),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
vpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
vpack: 'center',
|
||||
hpack: 'start',
|
||||
class_name: 'menu-button-name bluetooth',
|
||||
truncate: 'end',
|
||||
wrap: true,
|
||||
label: device.alias,
|
||||
}),
|
||||
Widget.Revealer({
|
||||
hpack: 'start',
|
||||
reveal_child: device.connected || device.paired,
|
||||
child: Widget.Box({
|
||||
hpack: 'start',
|
||||
children: Utils.merge(
|
||||
[
|
||||
showBattery.bind('value'),
|
||||
batteryIcon.bind('value'),
|
||||
batteryState.bind('value'),
|
||||
],
|
||||
(showBat, batIcon, batState) => {
|
||||
if (
|
||||
!showBat ||
|
||||
(batState === 'paired' && !device.paired) ||
|
||||
(batState === 'connected' && !device.connected)
|
||||
) {
|
||||
return [getConnectionStatusLabel(device)];
|
||||
}
|
||||
|
||||
return [
|
||||
getConnectionStatusLabel(device),
|
||||
Widget.Box({
|
||||
children: getBatteryInfo(device, batIcon),
|
||||
}),
|
||||
];
|
||||
},
|
||||
),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: 'end',
|
||||
children: device.connecting
|
||||
? [
|
||||
Widget.Spinner({
|
||||
vpack: 'start',
|
||||
class_name: 'spinner bluetooth',
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
connectedControls(device, conDevNames),
|
||||
],
|
||||
});
|
||||
}),
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
export { devices };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1,24 +0,0 @@
|
||||
import Window from 'types/widgets/window.js';
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { Devices } from './devices/index.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'bluetoothmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items bluetooth',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container bluetooth',
|
||||
child: Devices(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,36 +0,0 @@
|
||||
import DropdownMenu from 'modules/menus/shared/dropdown/index';
|
||||
import { TimeWidget } from './time/index';
|
||||
import { CalendarWidget } from './calendar';
|
||||
import { WeatherWidget } from './weather/index';
|
||||
import options from 'options';
|
||||
import Window from 'types/widgets/window';
|
||||
import { Attribute, Child } from 'lib/types/widget';
|
||||
|
||||
const { enabled: weatherEnabled } = options.menus.clock.weather;
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'calendarmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'calendar-menu-content',
|
||||
css: 'padding: 1px; margin: -1px;',
|
||||
vexpand: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'calendar-content-container',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'calendar-content-items',
|
||||
vertical: true,
|
||||
children: weatherEnabled.bind('value').as((isWeatherEnabled) => {
|
||||
return [TimeWidget(), CalendarWidget(), ...(isWeatherEnabled ? [WeatherWidget()] : [])];
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Weather, WeatherIconTitle } from 'lib/types/weather.js';
|
||||
import { Variable } from 'types/variable.js';
|
||||
import { weatherIcons } from 'modules/icons/weather.js';
|
||||
import { isValidWeatherIconTitle } from 'globals/weather';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
import { getNextEpoch } from '../utils';
|
||||
|
||||
export const HourlyIcon = (theWeather: Variable<Weather>, hoursFromNow: number): BoxWidget => {
|
||||
const getIconQuery = (wthr: Weather): WeatherIconTitle => {
|
||||
const nextEpoch = getNextEpoch(wthr, hoursFromNow);
|
||||
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
|
||||
|
||||
if (weatherAtEpoch === undefined) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
let iconQuery = weatherAtEpoch.condition.text.trim().toLowerCase().replaceAll(' ', '_');
|
||||
|
||||
if (!weatherAtEpoch?.is_day && iconQuery === 'partly_cloudy') {
|
||||
iconQuery = 'partly_cloudy_night';
|
||||
}
|
||||
|
||||
if (isValidWeatherIconTitle(iconQuery)) {
|
||||
return iconQuery;
|
||||
} else {
|
||||
return 'warning';
|
||||
}
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
hpack: 'center',
|
||||
child: theWeather.bind('value').as((w) => {
|
||||
const iconQuery = getIconQuery(w);
|
||||
const weatherIcn = weatherIcons[iconQuery] || weatherIcons['warning'];
|
||||
|
||||
return Widget.Label({
|
||||
hpack: 'center',
|
||||
class_name: 'hourly-weather-icon txt-icon',
|
||||
label: weatherIcn,
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import { HourlyIcon } from './icon/index.js';
|
||||
import { HourlyTemp } from './temperature/index.js';
|
||||
import { HourlyTime } from './time/index.js';
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
|
||||
export const Hourly = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
vertical: false,
|
||||
hexpand: true,
|
||||
hpack: 'fill',
|
||||
class_name: 'hourly-weather-container',
|
||||
children: [1, 2, 3, 4].map((hoursFromNow) => {
|
||||
return Widget.Box({
|
||||
class_name: 'hourly-weather-item',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
HourlyTime(theWeather, hoursFromNow),
|
||||
HourlyIcon(theWeather, hoursFromNow),
|
||||
HourlyTemp(theWeather, hoursFromNow),
|
||||
],
|
||||
});
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import options from 'options';
|
||||
import Label from 'types/widgets/label';
|
||||
import { Child } from 'lib/types/widget';
|
||||
import { getNextEpoch } from '../utils';
|
||||
|
||||
const { unit } = options.menus.clock.weather;
|
||||
|
||||
export const HourlyTemp = (theWeather: Variable<Weather>, hoursFromNow: number): Label<Child> => {
|
||||
return Widget.Label({
|
||||
class_name: 'hourly-weather-temp',
|
||||
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
|
||||
if (!Object.keys(wthr).length) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const nextEpoch = getNextEpoch(wthr, hoursFromNow);
|
||||
const weatherAtEpoch = wthr.forecast.forecastday[0].hour.find((h) => h.time_epoch === nextEpoch);
|
||||
|
||||
if (unt === 'imperial') {
|
||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_f) : '-'}° F`;
|
||||
}
|
||||
return `${weatherAtEpoch ? Math.ceil(weatherAtEpoch.temp_c) : '-'}° C`;
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Child } from 'lib/types/widget';
|
||||
import { Variable } from 'types/variable';
|
||||
import Label from 'types/widgets/label';
|
||||
import { getNextEpoch } from '../utils';
|
||||
|
||||
export const HourlyTime = (theWeather: Variable<Weather>, hoursFromNow: number): Label<Child> => {
|
||||
return Widget.Label({
|
||||
class_name: 'hourly-weather-time',
|
||||
label: theWeather.bind('value').as((w) => {
|
||||
if (!Object.keys(w).length) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const nextEpoch = getNextEpoch(w, hoursFromNow);
|
||||
const dateAtEpoch = new Date(nextEpoch * 1000);
|
||||
let hours = dateAtEpoch.getHours();
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12 || 12;
|
||||
|
||||
return `${hours}${ampm}`;
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import { getWeatherStatusTextIcon } from 'globals/weather.js';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
export const TodayIcon = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
vpack: 'center',
|
||||
hpack: 'start',
|
||||
class_name: 'calendar-menu-weather today icon container',
|
||||
child: Widget.Label({
|
||||
class_name: 'calendar-menu-weather today icon txt-icon',
|
||||
label: theWeather.bind('value').as((w) => {
|
||||
return getWeatherStatusTextIcon(w);
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import options from 'options';
|
||||
import { Unit } from 'lib/types/options';
|
||||
import { getRainChance, getWindConditions } from 'globals/weather';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
|
||||
const { unit } = options.menus.clock.weather;
|
||||
|
||||
export const TodayStats = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'calendar-menu-weather today stats container',
|
||||
hpack: 'end',
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'weather wind',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'weather wind icon txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'weather wind label',
|
||||
label: Utils.merge(
|
||||
[theWeather.bind('value'), unit.bind('value')],
|
||||
(wthr: Weather, unt: Unit) => {
|
||||
return getWindConditions(wthr, unt);
|
||||
},
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'weather precip',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'weather precip icon txt-icon',
|
||||
label: '',
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: 'weather precip label',
|
||||
label: theWeather.bind('value').as((v) => getRainChance(v)),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Weather } from 'lib/types/weather';
|
||||
import { Variable } from 'types/variable';
|
||||
import options from 'options';
|
||||
import { getTemperature, getWeatherIcon } from 'globals/weather';
|
||||
import { BoxWidget } from 'lib/types/widget';
|
||||
const { unit } = options.menus.clock.weather;
|
||||
|
||||
export const TodayTemperature = (theWeather: Variable<Weather>): BoxWidget => {
|
||||
return Widget.Box({
|
||||
hpack: 'center',
|
||||
vpack: 'center',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
vpack: 'center',
|
||||
class_name: 'calendar-menu-weather today temp container',
|
||||
vertical: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hexpand: true,
|
||||
hpack: 'center',
|
||||
children: [
|
||||
Widget.Label({
|
||||
class_name: 'calendar-menu-weather today temp label',
|
||||
label: Utils.merge([theWeather.bind('value'), unit.bind('value')], (wthr, unt) => {
|
||||
return getTemperature(wthr, unt);
|
||||
}),
|
||||
}),
|
||||
Widget.Label({
|
||||
class_name: theWeather
|
||||
.bind('value')
|
||||
.as(
|
||||
(v) =>
|
||||
`calendar-menu-weather today temp label icon txt-icon ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
|
||||
),
|
||||
label: theWeather
|
||||
.bind('value')
|
||||
.as((v) => getWeatherIcon(Math.ceil(v.current.temp_f)).icon),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
hpack: 'center',
|
||||
child: Widget.Label({
|
||||
maxWidthChars: 15,
|
||||
truncate: 'end',
|
||||
lines: 2,
|
||||
class_name: theWeather
|
||||
.bind('value')
|
||||
.as(
|
||||
(v) =>
|
||||
`calendar-menu-weather today condition label ${getWeatherIcon(Math.ceil(v.current.temp_f)).color}`,
|
||||
),
|
||||
label: theWeather.bind('value').as((v) => v.current.condition.text),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1,52 +0,0 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { Profile } from './profile/index.js';
|
||||
import { Shortcuts } from './shortcuts/index.js';
|
||||
import { Controls } from './controls/index.js';
|
||||
import { Stats } from './stats/index.js';
|
||||
import { Directories } from './directories/index.js';
|
||||
import Window from 'types/widgets/window.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import options from 'options.js';
|
||||
|
||||
const { controls, shortcuts, stats, directories } = options.menus.dashboard;
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'dashboardmenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'dashboard-menu-content',
|
||||
css: 'padding: 1px; margin: -1px;',
|
||||
vexpand: false,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'dashboard-content-container',
|
||||
vertical: true,
|
||||
children: Utils.merge(
|
||||
[
|
||||
controls.enabled.bind('value'),
|
||||
shortcuts.enabled.bind('value'),
|
||||
stats.enabled.bind('value'),
|
||||
directories.enabled.bind('value'),
|
||||
],
|
||||
(isControlsEnabled, isShortcutsEnabled, isStatsEnabled, isDirectoriesEnabled) => {
|
||||
return [
|
||||
Widget.Box({
|
||||
class_name: 'dashboard-content-items',
|
||||
vertical: true,
|
||||
children: [
|
||||
Profile(),
|
||||
...(isShortcutsEnabled ? [Shortcuts()] : []),
|
||||
...(isControlsEnabled ? [Controls()] : []),
|
||||
...(isDirectoriesEnabled ? [Directories()] : []),
|
||||
...(isStatsEnabled ? [Stats()] : []),
|
||||
],
|
||||
}),
|
||||
];
|
||||
},
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
import powermenu from '../../power/helpers/actions.js';
|
||||
import { PowerOptions } from 'lib/types/options.js';
|
||||
import GdkPixbuf from 'gi://GdkPixbuf';
|
||||
|
||||
import options from 'options';
|
||||
import { BoxWidget, Child } from 'lib/types/widget.js';
|
||||
import Label from 'types/widgets/label.js';
|
||||
const { image, name } = options.menus.dashboard.powermenu.avatar;
|
||||
const { confirmation, shutdown, logout, sleep, reboot } = options.menus.dashboard.powermenu;
|
||||
|
||||
const Profile = (): BoxWidget => {
|
||||
const handleClick = (action: PowerOptions): void => {
|
||||
const actions = {
|
||||
shutdown: shutdown.value,
|
||||
reboot: reboot.value,
|
||||
logout: logout.value,
|
||||
sleep: sleep.value,
|
||||
};
|
||||
App.closeWindow('dashboardmenu');
|
||||
|
||||
if (!confirmation.value) {
|
||||
Utils.execAsync(actions[action]).catch((err) =>
|
||||
console.error(`Failed to execute ${action} command. Error: ${err}`),
|
||||
);
|
||||
} else {
|
||||
powermenu.action(action);
|
||||
}
|
||||
};
|
||||
|
||||
const getIconForButton = (txtIcon: string): Label<Child> => {
|
||||
return Widget.Label({
|
||||
className: 'txt-icon',
|
||||
label: txtIcon,
|
||||
});
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'profiles-container',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'profile-picture-container dashboard-card',
|
||||
hexpand: true,
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
hpack: 'center',
|
||||
class_name: 'profile-picture',
|
||||
css: image.bind('value').as((i) => {
|
||||
try {
|
||||
GdkPixbuf.Pixbuf.new_from_file(i);
|
||||
return `background-image: url("${i}")`;
|
||||
} catch {
|
||||
return `background-image: url("${App.configDir}/assets/hyprpanel.png")`;
|
||||
}
|
||||
}),
|
||||
}),
|
||||
Widget.Label({
|
||||
hpack: 'center',
|
||||
class_name: 'profile-name',
|
||||
label: name.bind('value').as((v) => {
|
||||
if (v === 'system') {
|
||||
return Utils.exec('bash -c whoami');
|
||||
}
|
||||
return v;
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Box({
|
||||
class_name: 'power-menu-container dashboard-card',
|
||||
vertical: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button shutdown',
|
||||
on_clicked: () => handleClick('shutdown'),
|
||||
tooltip_text: 'Shut Down',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button restart',
|
||||
on_clicked: () => handleClick('reboot'),
|
||||
tooltip_text: 'Restart',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button lock',
|
||||
on_clicked: () => handleClick('logout'),
|
||||
tooltip_text: 'Log Out',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
Widget.Button({
|
||||
class_name: 'dashboard-button sleep',
|
||||
on_clicked: () => handleClick('sleep'),
|
||||
tooltip_text: 'Sleep',
|
||||
vexpand: true,
|
||||
child: getIconForButton(''),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Profile };
|
||||
@@ -1,326 +0,0 @@
|
||||
const hyprland = await Service.import('hyprland');
|
||||
import { BashPoller } from 'lib/poller/BashPoller';
|
||||
import { Attribute, BoxWidget, Child } from 'lib/types/widget';
|
||||
import options from 'options';
|
||||
import { Variable as VarType } from 'types/variable';
|
||||
import Box from 'types/widgets/box';
|
||||
import Button from 'types/widgets/button';
|
||||
import Label from 'types/widgets/label';
|
||||
|
||||
const { left, right } = options.menus.dashboard.shortcuts;
|
||||
|
||||
const Shortcuts = (): BoxWidget => {
|
||||
const pollingInterval = Variable(1000);
|
||||
const isRecording = Variable(false);
|
||||
|
||||
const handleRecorder = (commandOutput: string): boolean => {
|
||||
if (commandOutput === 'recording') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const recordingPoller = new BashPoller<boolean, []>(
|
||||
isRecording,
|
||||
[],
|
||||
pollingInterval.bind('value'),
|
||||
`${App.configDir}/services/screen_record.sh status`,
|
||||
handleRecorder,
|
||||
);
|
||||
|
||||
recordingPoller.initialize();
|
||||
|
||||
const handleClick = (action: string, tOut: number = 250): void => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
|
||||
setTimeout(() => {
|
||||
Utils.execAsync(action)
|
||||
.then((res) => {
|
||||
return res;
|
||||
})
|
||||
.catch((err) => err);
|
||||
}, tOut);
|
||||
};
|
||||
|
||||
const recordingDropdown = Widget.Menu({
|
||||
class_name: 'dropdown recording',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
setup: (self) => {
|
||||
const renderMonitorList = (): void => {
|
||||
const displays = hyprland.monitors.map((mon) => {
|
||||
return Widget.MenuItem({
|
||||
label: `Display ${mon.name}`,
|
||||
on_activate: () => {
|
||||
App.closeWindow('dashboardmenu');
|
||||
Utils.execAsync(`${App.configDir}/services/screen_record.sh start ${mon.name}`).catch(
|
||||
(err) => console.error(err),
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// NOTE: This is disabled since window recording isn't available on wayland
|
||||
// const apps = hyprland.clients.map((clt) => {
|
||||
// return Widget.MenuItem({
|
||||
// label: `${clt.class.charAt(0).toUpperCase() + clt.class.slice(1)} (Workspace ${clt.workspace.name})`,
|
||||
// on_activate: () => {
|
||||
// App.closeWindow('dashboardmenu');
|
||||
// Utils.execAsync(
|
||||
// `${App.configDir}/services/screen_record.sh start ${clt.focusHistoryID}`,
|
||||
// ).catch((err) => console.error(err));
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
self.children = [
|
||||
...displays,
|
||||
// Disabled since window recording isn't available on wayland
|
||||
// ...apps
|
||||
];
|
||||
};
|
||||
self.hook(hyprland, renderMonitorList, 'monitor-added');
|
||||
self.hook(hyprland, renderMonitorList, 'monitor-removed');
|
||||
},
|
||||
});
|
||||
|
||||
type ShortcutFixed = {
|
||||
tooltip: string;
|
||||
command: string;
|
||||
icon: string;
|
||||
configurable: false;
|
||||
};
|
||||
|
||||
type ShortcutVariable = {
|
||||
tooltip: VarType<string>;
|
||||
command: VarType<string>;
|
||||
icon: VarType<string>;
|
||||
configurable?: true;
|
||||
};
|
||||
|
||||
type Shortcut = ShortcutFixed | ShortcutVariable;
|
||||
|
||||
const cmdLn = (sCut: ShortcutVariable): boolean => {
|
||||
return sCut.command.value.length > 0;
|
||||
};
|
||||
|
||||
const leftCardHidden = Variable(
|
||||
!(cmdLn(left.shortcut1) || cmdLn(left.shortcut2) || cmdLn(left.shortcut3) || cmdLn(left.shortcut4)),
|
||||
);
|
||||
|
||||
const createButton = (shortcut: Shortcut, className: string): Button<Label<Attribute>, Attribute> => {
|
||||
if (shortcut.configurable !== false) {
|
||||
return Widget.Button({
|
||||
vexpand: true,
|
||||
tooltip_text: shortcut.tooltip.value,
|
||||
class_name: className,
|
||||
on_primary_click: () => handleClick(shortcut.command.value),
|
||||
child: Widget.Label({
|
||||
class_name: 'button-label txt-icon',
|
||||
label: shortcut.icon.value,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
// handle non-configurable shortcut
|
||||
return Widget.Button({
|
||||
vexpand: true,
|
||||
tooltip_text: shortcut.tooltip,
|
||||
class_name: className,
|
||||
on_primary_click: (_, event) => {
|
||||
if (shortcut.command === 'settings-dialog') {
|
||||
App.closeWindow('dashboardmenu');
|
||||
App.toggleWindow('settings-dialog');
|
||||
} else if (shortcut.command === 'record') {
|
||||
if (isRecording.value === true) {
|
||||
App.closeWindow('dashboardmenu');
|
||||
return Utils.execAsync(`${App.configDir}/services/screen_record.sh stop`).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
} else {
|
||||
recordingDropdown.popup_at_pointer(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Widget.Label({
|
||||
class_name: 'button-label txt-icon',
|
||||
label: shortcut.icon,
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createButtonIfCommandExists = (
|
||||
shortcut: Shortcut,
|
||||
className: string,
|
||||
command: string,
|
||||
): Button<Label<Attribute>, Attribute> | Box<Child, Attribute> => {
|
||||
if (command.length > 0) {
|
||||
return createButton(shortcut, className);
|
||||
}
|
||||
return Widget.Box();
|
||||
};
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'shortcuts-container',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
child: Utils.merge(
|
||||
[
|
||||
left.shortcut1.command.bind('value'),
|
||||
left.shortcut2.command.bind('value'),
|
||||
left.shortcut1.tooltip.bind('value'),
|
||||
left.shortcut2.tooltip.bind('value'),
|
||||
left.shortcut1.icon.bind('value'),
|
||||
left.shortcut2.icon.bind('value'),
|
||||
left.shortcut3.command.bind('value'),
|
||||
left.shortcut4.command.bind('value'),
|
||||
left.shortcut3.tooltip.bind('value'),
|
||||
left.shortcut4.tooltip.bind('value'),
|
||||
left.shortcut3.icon.bind('value'),
|
||||
left.shortcut4.icon.bind('value'),
|
||||
],
|
||||
() => {
|
||||
const isVisibleLeft = cmdLn(left.shortcut1) || cmdLn(left.shortcut2);
|
||||
const isVisibleRight = cmdLn(left.shortcut3) || cmdLn(left.shortcut4);
|
||||
|
||||
if (!isVisibleLeft && !isVisibleRight) {
|
||||
leftCardHidden.value = true;
|
||||
return Widget.Box();
|
||||
}
|
||||
|
||||
leftCardHidden.value = false;
|
||||
|
||||
return Widget.Box({
|
||||
class_name: 'container most-used dashboard-card',
|
||||
children: [
|
||||
Widget.Box({
|
||||
className: `card-button-section-container ${isVisibleRight && isVisibleLeft ? 'visible' : ''}`,
|
||||
child: isVisibleLeft
|
||||
? Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut1,
|
||||
`dashboard-button top-button ${cmdLn(left.shortcut2) ? 'paired' : ''}`,
|
||||
left.shortcut1.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut2,
|
||||
'dashboard-button',
|
||||
left.shortcut2.command.value,
|
||||
),
|
||||
],
|
||||
})
|
||||
: Widget.Box({
|
||||
children: [],
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
className: 'card-button-section-container',
|
||||
child: isVisibleRight
|
||||
? Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut3,
|
||||
`dashboard-button top-button ${cmdLn(left.shortcut4) ? 'paired' : ''}`,
|
||||
left.shortcut3.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
left.shortcut4,
|
||||
'dashboard-button',
|
||||
left.shortcut4.command.value,
|
||||
),
|
||||
],
|
||||
})
|
||||
: Widget.Box({
|
||||
children: [],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
),
|
||||
}),
|
||||
Widget.Box({
|
||||
child: Utils.merge(
|
||||
[
|
||||
right.shortcut1.command.bind('value'),
|
||||
right.shortcut1.tooltip.bind('value'),
|
||||
right.shortcut1.icon.bind('value'),
|
||||
right.shortcut3.command.bind('value'),
|
||||
right.shortcut3.tooltip.bind('value'),
|
||||
right.shortcut3.icon.bind('value'),
|
||||
leftCardHidden.bind('value'),
|
||||
isRecording.bind('value'),
|
||||
],
|
||||
() => {
|
||||
return Widget.Box({
|
||||
class_name: `container utilities dashboard-card ${!leftCardHidden.value ? 'paired' : ''}`,
|
||||
children: [
|
||||
Widget.Box({
|
||||
className: `card-button-section-container visible`,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
right.shortcut1,
|
||||
'dashboard-button top-button paired',
|
||||
right.shortcut1.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
{
|
||||
tooltip: 'HyprPanel Configuration',
|
||||
command: 'settings-dialog',
|
||||
icon: '',
|
||||
configurable: false,
|
||||
},
|
||||
'dashboard-button',
|
||||
'settings-dialog',
|
||||
),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
Widget.Box({
|
||||
className: 'card-button-section-container',
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
children: [
|
||||
createButtonIfCommandExists(
|
||||
right.shortcut3,
|
||||
'dashboard-button top-button paired',
|
||||
right.shortcut3.command.value,
|
||||
),
|
||||
createButtonIfCommandExists(
|
||||
{
|
||||
tooltip: 'Record Screen',
|
||||
command: 'record',
|
||||
icon: '',
|
||||
configurable: false,
|
||||
},
|
||||
`dashboard-button record ${isRecording.value ? 'active' : ''}`,
|
||||
'record',
|
||||
),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
},
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Shortcuts };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -1,25 +0,0 @@
|
||||
import DropdownMenu from '../shared/dropdown/index.js';
|
||||
import { EnergyProfiles } from './profiles/index.js';
|
||||
import { Brightness } from './brightness/index.js';
|
||||
import { Attribute, Child } from 'lib/types/widget.js';
|
||||
import Window from 'types/widgets/window.js';
|
||||
import options from 'options.js';
|
||||
|
||||
export default (): Window<Child, Attribute> => {
|
||||
return DropdownMenu({
|
||||
name: 'energymenu',
|
||||
transition: options.menus.transition.bind('value'),
|
||||
child: Widget.Box({
|
||||
class_name: 'menu-items energy',
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
hpack: 'fill',
|
||||
hexpand: true,
|
||||
class_name: 'menu-items-container energy',
|
||||
children: [Brightness(), EnergyProfiles()],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -1,22 +0,0 @@
|
||||
import { BoxWidget } from 'lib/types/widget.js';
|
||||
import { shuffleControl } from './shuffle/index.js';
|
||||
import { previousTrack } from './previous/index.js';
|
||||
import { playPause } from './playpause/index.js';
|
||||
import { nextTrack } from './next/index.js';
|
||||
import { loopControl } from './loop/index.js';
|
||||
|
||||
const Controls = (): BoxWidget => {
|
||||
return Widget.Box({
|
||||
class_name: 'media-indicator-current-player-controls',
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
class_name: 'media-indicator-current-controls',
|
||||
hpack: 'center',
|
||||
children: [shuffleControl(), previousTrack(), playPause(), nextTrack(), loopControl()],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export { Controls };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user