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