diff --git a/flake.lock b/flake.lock index 05d5d622b..dfab39740 100644 --- a/flake.lock +++ b/flake.lock @@ -2,34 +2,73 @@ "nodes": { "ags": { "inputs": { - "nixpkgs": "nixpkgs", - "systems": "systems" + "astal": "astal", + "nixpkgs": "nixpkgs" }, "locked": { - "lastModified": 1728326430, - "narHash": "sha256-tV1ABHuA1HItMdCTuNdA8fMB+qw7LpjvI945VwMSABI=", - "owner": "Aylur", + "lastModified": 1734091628, + "narHash": "sha256-8O3i8zESjHVsGzyXb8gEpLztvANq3Ot5bwo60YKJc7k=", + "owner": "aylur", "repo": "ags", - "rev": "60180a184cfb32b61a1d871c058b31a3b9b0743d", + "rev": "27cd93147aba09142fa585fd16f13c56268b696c", "type": "github" }, "original": { - "owner": "Aylur", + "owner": "aylur", "repo": "ags", "type": "github" } }, + "astal": { + "inputs": { + "nixpkgs": [ + "ags", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733520119, + "narHash": "sha256-6K07ZJTnFu1xASBCMtVc9cFTbBEauwSc7gGBmjLkLSk=", + "owner": "aylur", + "repo": "astal", + "rev": "4c19d8d06fa25cc6389f37abe8839b4d8be5c0d6", + "type": "github" + }, + "original": { + "owner": "aylur", + "repo": "astal", + "type": "github" + } + }, + "astal_2": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1734814417, + "narHash": "sha256-R+tLGIxlaqsOmV52TdXHP0u33q5PdJ77gtiUPK5BbMg=", + "owner": "aylur", + "repo": "astal", + "rev": "3468763d51d389c67ec7b1a390ffa8a5328bddb6", + "type": "github" + }, + "original": { + "owner": "aylur", + "repo": "astal", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1725634671, - "narHash": "sha256-v3rIhsJBOMLR8e/RNWxr828tB+WywYIoajrZKFM+0Gg=", - "owner": "NixOS", + "lastModified": 1733581040, + "narHash": "sha256-Qn3nPMSopRQJgmvHzVqPcE3I03zJyl8cSbgnnltfFDY=", + "owner": "nixos", "repo": "nixpkgs", - "rev": "574d1eac1c200690e27b8eb4e24887f8df7ac27c", + "rev": "22c3f2cf41a0e70184334a958e6b124fb0ce3e01", "type": "github" }, "original": { - "owner": "NixOS", + "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" @@ -37,11 +76,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1732758367, - "narHash": "sha256-RzaI1RO0UXqLjydtz3GAXSTzHkpb/lLD1JD8a0W4Wpo=", + "lastModified": 1734424634, + "narHash": "sha256-cHar1vqHOOyC7f1+tVycPoWTfKIaqkoe1Q6TnKzuti4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fa42b5a5f401aab8a32bd33c9a4de0738180dc59", + "rev": "d3c42f187194c26d9f0309a8ecc469d6c878ce33", "type": "github" }, "original": { @@ -51,26 +90,28 @@ "type": "github" } }, - "root": { - "inputs": { - "ags": "ags", - "nixpkgs": "nixpkgs_2" - } - }, - "systems": { + "nixpkgs_3": { "locked": { - "lastModified": 1689347949, - "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", - "owner": "nix-systems", - "repo": "default-linux", - "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "lastModified": 1734649271, + "narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d70bd19e0a38ad4790d3913bf08fcbfc9eeca507", "type": "github" }, "original": { - "owner": "nix-systems", - "repo": "default-linux", + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", "type": "github" } + }, + "root": { + "inputs": { + "ags": "ags", + "astal": "astal_2", + "nixpkgs": "nixpkgs_3" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 685fed799..4ef7b3da7 100644 --- a/flake.nix +++ b/flake.nix @@ -1,78 +1,85 @@ { - description = "A Bar/Panel for Hyprland with extensive customizability."; - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; - ags.url = "github:Aylur/ags"; + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + ags.url = "github:aylur/ags"; + astal.url = "github:aylur/astal"; }; - outputs = inputs: let + outputs = { self, nixpkgs, ags, astal }: let systems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux" ]; - forEachSystem = inputs.nixpkgs.lib.genAttrs systems; - pkgsFor = forEachSystem ( - system: - import inputs.nixpkgs { - inherit system; - config.allowUnfree = true; - } - ); - - devShellFor = system: - inputs.nixpkgs.lib.genAttrs ["default"] ( - _: - inputs.nixpkgs.legacyPackages.${system}.mkShell { - buildInputs = [ - pkgsFor.${system}.esbuild - pkgsFor.${system}.fish - pkgsFor.${system}.typescript - pkgsFor.${system}.bun - pkgsFor.${system}.libnotify - pkgsFor.${system}.dart-sass - pkgsFor.${system}.fd - pkgsFor.${system}.btop - pkgsFor.${system}.bluez - pkgsFor.${system}.libgtop - pkgsFor.${system}.gobject-introspection - pkgsFor.${system}.glib - pkgsFor.${system}.bluez-tools - pkgsFor.${system}.grimblast - pkgsFor.${system}.gpu-screen-recorder - pkgsFor.${system}.brightnessctl - pkgsFor.${system}.gnome-bluetooth - pkgsFor.${system}.python3 - pkgsFor.${system}.matugen - inputs.ags.packages.${system}.agsWithTypes - ]; - nativeBuildInputs = with pkgsFor.${system}; [ - nixfmt-rfc-style - nil - ]; - shellHook = '' - export GDK_BACKEND=wayland - export GI_TYPELIB_PATH=${pkgsFor.${system}.libgtop}/lib/girepository-1.0:${pkgsFor.${system}.glib}/lib/girepository-1.0:$GI_TYPELIB_PATH - ''; - } - ); + forEachSystem = nixpkgs.lib.genAttrs systems; in { - devShells = forEachSystem devShellFor; + packages = forEachSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + default = ags.lib.bundle { + inherit pkgs; + src = ./.; + name = "hyprpanel"; # name of executable + entry = "app.ts"; + + # additional libraries and executables to add to gjs' runtime + extraPackages = [ + ags.packages.${system}.agsFull + astal.packages.${system}.tray + astal.packages.${system}.hyprland + astal.packages.${system}.io + astal.packages.${system}.apps + astal.packages.${system}.battery + astal.packages.${system}.bluetooth + astal.packages.${system}.mpris + astal.packages.${system}.network + astal.packages.${system}.notifd + astal.packages.${system}.powerprofiles + astal.packages.${system}.wireplumber + pkgs.fish + pkgs.typescript + pkgs.libnotify + pkgs.dart-sass + pkgs.fd + pkgs.btop + pkgs.bluez + pkgs.libgtop + pkgs.gobject-introspection + pkgs.glib + pkgs.bluez-tools + pkgs.grimblast + pkgs.gpu-screen-recorder + pkgs.brightnessctl + pkgs.gnome-bluetooth + (pkgs.python3.withPackages (python-pkgs: with python-pkgs; [ + gpustat + dbus-python + pygobject3 + ])) + pkgs.matugen + pkgs.hyprpicker + pkgs.hyprsunset + pkgs.hypridle + pkgs.wireplumber + pkgs.networkmanager + pkgs.upower + pkgs.gvfs + pkgs.swww + pkgs.pywal + ]; + }; + }); + # Define .overlay to expose the package as pkgs.hyprpanel based on the system overlay = final: prev: { - hyprpanel = - if final ? callPackage - then (final.callPackage ./nix {inherit inputs;}).desktop.script - else inputs.self.packages.${prev.stdenv.system}.default; + hyprpanel = prev.writeShellScriptBin "hyprpanel" '' + if [ "$#" -eq 0 ]; then + exec ${self.packages.${final.stdenv.system}.default}/bin/hyprpanel + else + exec ${astal.packages.${final.stdenv.system}.io}/bin/astal -i hyprpanel "$@" + fi + ''; }; - packages = forEachSystem ( - system: let - pkgs = pkgsFor.${system}; - in { - default = (pkgs.callPackage ./nix {inherit inputs;}).desktop.script; - } - ); }; } diff --git a/nix/default.nix b/nix/default.nix deleted file mode 100644 index 7c54e8130..000000000 --- a/nix/default.nix +++ /dev/null @@ -1,65 +0,0 @@ -{ - inputs, - pkgs, - system, - stdenv, - lib, - writeShellScriptBin, - bun, - dart-sass, - fd, - accountsservice, - btop, - pipewire, - bluez, - bluez-tools, - grimblast, - gpu-screen-recorder, - networkmanager, - brightnessctl, - gnome-bluetooth, - matugen, - swww, - python3, - libgtop, - gobject-introspection, - glib, -}: let - ags = inputs.ags.packages.${system}.default.override { - extraPackages = [accountsservice]; - }; - - pname = "hyprpanel"; - config = stdenv.mkDerivation { - inherit pname; - version = "latest"; - src = ../.; - - buildPhase = '' - ${bun}/bin/bun build ./main.ts \ - --outfile main.js \ - --external "resource://*" \ - --external "gi://*" - ''; - - installPhase = '' - mkdir $out - cp -r assets $out - cp -r scss $out - cp -r widget $out - cp -r services $out - cp -r themes $out - cp -r scripts $out - cp -f main.js $out/config.js - ''; - }; -in { - desktop = { - inherit config; - script = writeShellScriptBin pname '' - export PATH=$PATH:${lib.makeBinPath [dart-sass fd btop pipewire bluez bluez-tools networkmanager matugen swww grimblast gpu-screen-recorder brightnessctl gnome-bluetooth python3]} - export GI_TYPELIB_PATH=${libgtop}/lib/girepository-1.0:${glib}/lib/girepository-1.0:$GI_TYPELIB_PATH - ${ags}/bin/ags -b hyprpanel -c ${config}/config.js $@ - ''; - }; -} diff --git a/scripts/checkUpdates.sh b/scripts/checkUpdates.sh index 39bf6ecb4..ab36d3c5a 100755 --- a/scripts/checkUpdates.sh +++ b/scripts/checkUpdates.sh @@ -3,14 +3,19 @@ check_arch_updates() { official_updates=0 aur_updates=0 + if command -v paru &> /dev/null; then + aur_helper="paru" + else + aur_helper="yay" + fi if [ "$1" = "-y" ]; then - aur_updates=$(yay -Qum 2>/dev/null | wc -l) + aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l) elif [ "$1" = "-p" ]; then official_updates=$(checkupdates 2>/dev/null | wc -l) else official_updates=$(checkupdates 2>/dev/null | wc -l) - aur_updates=$(yay -Qum 2>/dev/null | wc -l) + aur_updates=$($aur_helper -Qum 2>/dev/null | wc -l) fi total_updates=$((official_updates + aur_updates)) diff --git a/scripts/fillThemes.js b/scripts/fillThemes.js index cc8855ac0..2d8e3fe60 100644 --- a/scripts/fillThemes.js +++ b/scripts/fillThemes.js @@ -13,7 +13,7 @@ * If no themes_directory is provided, it defaults to '~/.config/ags/themes'. */ -const fs = require('fs'); +const fs = require('fs').promises; const path = require('path'); const os = require('os'); @@ -48,14 +48,14 @@ const COLORS = { const formatMessage = (color, message) => `${color}${message}${COLORS.RESET}`; /** - * Loads and parses a JSON file. + * Loads and parses a JSON file asynchronously. * * @param {string} filePath - The path to the JSON file. - * @returns {Object} The parsed JSON object. + * @returns {Promise} The parsed JSON object. */ -const loadJSON = (filePath) => { +const loadJSON = async (filePath) => { try { - const data = fs.readFileSync(filePath, 'utf8'); + const data = await fs.readFile(filePath, 'utf8'); return JSON.parse(data); } catch (error) { console.error(formatMessage(COLORS.FG_RED, `Error reading or parsing '${filePath}': ${error.message}`)); @@ -64,15 +64,15 @@ const loadJSON = (filePath) => { }; /** - * Saves a JSON object to a file with indentation. + * Saves a JSON object to a file with indentation asynchronously. * * @param {string} filePath - The path to the JSON file. * @param {Object} data - The JSON data to save. */ -const saveJSON = (filePath, data) => { +const saveJSON = async (filePath, data) => { try { const jsonString = JSON.stringify(data, null, 2); - fs.writeFileSync(filePath, jsonString, 'utf8'); + await fs.writeFile(filePath, jsonString, 'utf8'); } catch (error) { console.error(formatMessage(COLORS.FG_RED, `Error writing to '${filePath}': ${error.message}`)); process.exit(1); @@ -123,9 +123,7 @@ const findMissingKeys = (baseJSON, targetJSON) => { * @returns {boolean} True if the key is excluded, otherwise false. */ const isExcludedKey = (key) => { - const excludedPatterns = [ - // Add any regex patterns for keys to exclude here - ]; + const excludedPatterns = []; return excludedPatterns.some((pattern) => pattern.test(key)); }; @@ -175,14 +173,18 @@ const collectBorderColors = (baseJSON) => { * @param {Object} targetJSON - The target JSON object. * @param {Object} specialKeyMappings - A map of special keys to their source keys. * @param {string} currentKey - The key currently being processed. + * @param {Object} baseTheme - The base theme JSON object. * @returns {*} The best matching value or null if a random selection is needed. */ -const determineBestMatchValue = (baseValue, valueToKeysMap, targetJSON, specialKeyMappings, currentKey) => { - // Check if the current key is in special mappings +const determineBestMatchValue = (baseValue, valueToKeysMap, targetJSON, specialKeyMappings, currentKey, baseTheme) => { if (specialKeyMappings.hasOwnProperty(currentKey)) { const sourceKey = specialKeyMappings[currentKey]; if (targetJSON.hasOwnProperty(sourceKey)) { + console.log(formatMessage(COLORS.FG_CYAN, `šŸ” Found source key '${sourceKey}' in target JSON.`)); return targetJSON[sourceKey]; + } else if (baseTheme && baseTheme.hasOwnProperty(sourceKey)) { + console.log(formatMessage(COLORS.FG_CYAN, `šŸ” Found source key '${sourceKey}' in base theme.`)); + return baseTheme[sourceKey]; } else { console.warn( formatMessage( @@ -229,14 +231,16 @@ const findExtraKeys = (baseTheme, targetJSON) => { * * @param {string} themePath - The path to the theme file. */ -const backupTheme = (themePath) => { - const backupDir = path.join(path.dirname(themePath), 'backup'); - if (!fs.existsSync(backupDir)) { - fs.mkdirSync(backupDir); +const backupTheme = async (themePath) => { + try { + const backupDir = path.join(path.dirname(themePath), 'backup'); + await fs.mkdir(backupDir, { recursive: true }); + const backupPath = path.join(backupDir, path.basename(themePath)); + await fs.copyFile(themePath, backupPath); + console.log(formatMessage(COLORS.FG_CYAN, `Backup created at '${backupPath}'.`)); + } catch (error) { + console.error(formatMessage(COLORS.FG_RED, `āŒ Error creating backup for '${themePath}': ${error.message}`)); } - const backupPath = path.join(backupDir, path.basename(themePath)); - fs.copyFileSync(themePath, backupPath); - console.log(formatMessage(COLORS.FG_CYAN, `Backup created at '${backupPath}'.`)); }; /** @@ -246,9 +250,10 @@ const backupTheme = (themePath) => { * @param {Object} baseTheme - The base JSON object. * @param {boolean} dryRun - If true, no changes will be written to files. * @param {Object} specialKeyMappings - A map of special keys to their source keys. + * @returns {Promise} */ -const processTheme = (themePath, baseTheme, dryRun, specialKeyMappings = {}) => { - const themeJSON = loadJSON(themePath); +const processTheme = async (themePath, baseTheme, dryRun, specialKeyMappings = {}) => { + const themeJSON = await loadJSON(themePath); const missingKeys = findMissingKeys(baseTheme, themeJSON); let hasChanges = false; @@ -266,14 +271,21 @@ const processTheme = (themePath, baseTheme, dryRun, specialKeyMappings = {}) => const valueToKeysMap = buildValueToKeysMap(baseTheme); const borderColors = collectBorderColors(baseTheme); - missingKeys.forEach((key) => { + for (const key of missingKeys) { if (isExcludedKey(key)) { console.log(formatMessage(COLORS.FG_MAGENTA, `ā— Excluded key from addition: "${key}"`)); - return; + continue; } const baseValue = baseTheme[key]; - const bestValue = determineBestMatchValue(baseValue, valueToKeysMap, themeJSON, specialKeyMappings, key); + const bestValue = determineBestMatchValue( + baseValue, + valueToKeysMap, + themeJSON, + specialKeyMappings, + key, + baseTheme, + ); if (bestValue !== null) { themeJSON[key] = bestValue; @@ -281,7 +293,7 @@ const processTheme = (themePath, baseTheme, dryRun, specialKeyMappings = {}) => } else { if (borderColors.length === 0) { console.error(formatMessage(COLORS.FG_RED, 'āŒ Error: No border colors available to assign.')); - return; + continue; } const randomColor = borderColors[Math.floor(Math.random() * borderColors.length)]; themeJSON[key] = randomColor; @@ -294,7 +306,7 @@ const processTheme = (themePath, baseTheme, dryRun, specialKeyMappings = {}) => } hasChanges = true; - }); + } } const extraKeys = findExtraKeys(baseTheme, themeJSON); @@ -309,11 +321,11 @@ const processTheme = (themePath, baseTheme, dryRun, specialKeyMappings = {}) => ), ); - extraKeys.forEach((key) => { + for (const key of extraKeys) { delete themeJSON[key]; console.log(formatMessage(COLORS.FG_RED, `āž– Removed key: "${key}"`)); hasChanges = true; - }); + } } if (hasChanges) { @@ -325,8 +337,8 @@ const processTheme = (themePath, baseTheme, dryRun, specialKeyMappings = {}) => ), ); } else { - backupTheme(themePath); - saveJSON(themePath, themeJSON); + await backupTheme(themePath); + await saveJSON(themePath, themeJSON); console.log( formatMessage(COLORS.FG_GREEN, `āœ… Updated '${path.basename(themePath)}' with missing and extra keys.`), ); @@ -339,7 +351,7 @@ const processTheme = (themePath, baseTheme, dryRun, specialKeyMappings = {}) => /** * The main function that orchestrates the theme comparison and updating. */ -const main = () => { +const main = async () => { const args = process.argv.slice(2); const dryRunIndex = args.indexOf('--dry-run'); const dryRun = dryRunIndex !== -1; @@ -350,7 +362,9 @@ const main = () => { const themesDir = args[0] || path.join(os.homedir(), '.config', 'ags', 'themes'); - if (!fs.existsSync(themesDir)) { + try { + await fs.access(themesDir); + } catch (error) { console.error(formatMessage(COLORS.FG_RED, `āŒ Error: Themes directory '${themesDir}' does not exist.`)); process.exit(1); } @@ -362,72 +376,68 @@ const main = () => { const baseThemeSplitPath = path.join(themesDir, baseThemeSplitFile); const baseThemeVividPath = path.join(themesDir, baseThemeVividFile); - if (!fs.existsSync(baseThemePath)) { - console.error( - formatMessage(COLORS.FG_RED, `āŒ Error: Base theme '${baseThemeFile}' does not exist in '${themesDir}'.`), - ); - process.exit(1); - } - - if (!fs.existsSync(baseThemeSplitPath)) { - console.error( - formatMessage( - COLORS.FG_RED, - `āŒ Error: Base split theme '${baseThemeSplitFile}' does not exist in '${themesDir}'.`, - ), - ); - process.exit(1); - } + const baseThemes = [ + { file: baseThemeFile, path: baseThemePath }, + { file: baseThemeSplitFile, path: baseThemeSplitPath }, + { file: baseThemeVividFile, path: baseThemeVividPath }, + ]; - if (!fs.existsSync(baseThemeVividPath)) { - console.error( - formatMessage( - COLORS.FG_RED, - `āŒ Error: Base vivid theme '${baseThemeVividFile}' does not exist in '${themesDir}'.`, - ), - ); - process.exit(1); + for (const theme of baseThemes) { + try { + await fs.access(theme.path); + } catch (error) { + console.error( + formatMessage(COLORS.FG_RED, `āŒ Error: Base theme '${theme.file}' does not exist in '${themesDir}'.`), + ); + process.exit(1); + } } - const baseTheme = loadJSON(baseThemePath); - const baseThemeSplit = loadJSON(baseThemeSplitPath); - const baseThemeVivid = loadJSON(baseThemeVividPath); + const [baseTheme, baseThemeSplit, baseThemeVivid] = await Promise.all([ + loadJSON(baseThemePath), + loadJSON(baseThemeSplitPath), + loadJSON(baseThemeVividPath), + ]); - const themeFiles = fs.readdirSync(themesDir).filter((file) => file.endsWith('.json')); + const themeFiles = (await fs.readdir(themesDir)).filter((file) => file.endsWith('.json')); - // Define special key mappings - // Format: "target_key": "source_key" const specialKeyMappings = { - 'theme.bar.buttons.modules.hypridle.icon': 'theme.bar.buttons.modules.storage.icon', - 'theme.bar.buttons.modules.hypridle.background': 'theme.bar.buttons.modules.storage.background', - 'theme.bar.buttons.modules.hypridle.icon_background': 'theme.bar.buttons.modules.storage.icon_background', - 'theme.bar.buttons.modules.hypridle.text': 'theme.bar.buttons.modules.storage.text', - 'theme.bar.buttons.modules.hypridle.border': 'theme.bar.buttons.modules.storage.border', - // Add more special mappings here if needed + 'theme.bar.menus.menu.bluetooth.scroller.color': 'theme.bar.menus.menu.bluetooth.label.color', + 'theme.bar.menus.menu.network.scroller.color': 'theme.bar.menus.menu.network.label.color', }; - themeFiles.forEach((file) => { - if (file === baseThemeFile || file === baseThemeSplitFile || file === baseThemeVividFile) { - return; - } - - const themePath = path.join(themesDir, file); - let correspondingBaseTheme; + const queue = [...themeFiles].filter( + (file) => + !['catppuccin_mocha.json', 'catppuccin_mocha_split.json', 'catppuccin_mocha_vivid.json'].includes(file), + ); + + const processQueue = async () => { + while (queue.length > 0) { + const promises = []; + for (let i = 0; i < concurrencyLimit && queue.length > 0; i++) { + const file = queue.shift(); + const themePath = path.join(themesDir, file); + let correspondingBaseTheme; + + if (file.endsWith('_split.json')) { + correspondingBaseTheme = baseThemeSplit; + } else if (file.endsWith('_vivid.json')) { + correspondingBaseTheme = baseThemeVivid; + } else { + correspondingBaseTheme = baseTheme; + } - if (file.endsWith('_split.json')) { - correspondingBaseTheme = baseThemeSplit; - } else if (file.endsWith('_vivid.json')) { - correspondingBaseTheme = baseThemeVivid; - } else { - correspondingBaseTheme = baseTheme; + promises.push( + processTheme(themePath, correspondingBaseTheme, dryRun, specialKeyMappings).catch((error) => { + console.error(formatMessage(COLORS.FG_RED, `āŒ Error processing '${file}': ${error.message}`)); + }), + ); + } + await Promise.all(promises); } + }; - try { - processTheme(themePath, correspondingBaseTheme, dryRun, specialKeyMappings); - } catch (error) { - console.error(formatMessage(COLORS.FG_RED, `āŒ Error processing '${file}': ${error.message}`)); - } - }); + await processQueue(); console.log(formatMessage(COLORS.FG_GREEN, '\nšŸŽ‰ All themes have been processed.')); }; diff --git a/src/components/bar/modules/bluetooth/index.tsx b/src/components/bar/modules/bluetooth/index.tsx index 678405369..3da04cd5b 100644 --- a/src/components/bar/modules/bluetooth/index.tsx +++ b/src/components/bar/modules/bluetooth/index.tsx @@ -27,7 +27,7 @@ const Bluetooth = (): BarBoxChild => { }; const componentClassName = Variable.derive( - [options.theme.bar.buttons.style, options.bar.volume.label], + [options.theme.bar.buttons.style, options.bar.bluetooth.label], (style, showLabel) => { const styleMap = { default: 'style1', @@ -40,7 +40,7 @@ const Bluetooth = (): BarBoxChild => { ); const componentBinding = Variable.derive( - [bind(options.bar.volume.label), bind(bluetoothService, 'isPowered'), bind(bluetoothService, 'devices')], + [bind(options.bar.bluetooth.label), bind(bluetoothService, 'isPowered'), bind(bluetoothService, 'devices')], (showLabel: boolean, isPowered: boolean, devices: AstalBluetooth.Device[]): JSX.Element[] => { if (showLabel) { return [btIcon(isPowered), btText(isPowered, devices)]; diff --git a/src/components/bar/modules/media/index.tsx b/src/components/bar/modules/media/index.tsx index 8c79c4066..7fd1a7d80 100644 --- a/src/components/bar/modules/media/index.tsx +++ b/src/components/bar/modules/media/index.tsx @@ -14,14 +14,14 @@ import { activePlayer, mediaAlbum, mediaArtist, mediaTitle } from 'src/globals/m const { truncation, truncation_size, show_label, show_active_only, rightClick, middleClick, format } = options.bar.media; -const Media = (): BarBoxChild => { - activePlayer.set(mprisService.get_players()[0]); +const isVis = Variable(!show_active_only.get()); - const isVis = Variable(!show_active_only.get()); +Variable.derive([bind(show_active_only), bind(mprisService, 'players')], (showActive, players) => { + isVis.set(!showActive || players?.length > 0); +}); - show_active_only.subscribe(() => { - isVis.set(!show_active_only.get() || mprisService.get_players().length > 0); - }); +const Media = (): BarBoxChild => { + activePlayer.set(mprisService.get_players()[0]); const songIcon = Variable(''); @@ -55,7 +55,6 @@ const Media = (): BarBoxChild => { { - isVis.drop(); songIcon.drop(); mediaLabel.drop(); componentClassName.drop(); diff --git a/src/components/bar/modules/netstat/index.tsx b/src/components/bar/modules/netstat/index.tsx index 8a515e613..ae0f7f5be 100644 --- a/src/components/bar/modules/netstat/index.tsx +++ b/src/components/bar/modules/netstat/index.tsx @@ -19,6 +19,8 @@ const { rateUnit, dynamicIcon, icon, + networkInLabel, + networkOutLabel, round, leftClick, rightClick, @@ -47,11 +49,11 @@ export const Netstat = (): BarBoxChild => { const renderNetworkLabel = (lblType: NetstatLabelType, networkService: NetworkResourceData): string => { switch (lblType) { case 'in': - return `ā†“ ${networkService.in}`; + return `${networkInLabel.get()} ${networkService.in}`; case 'out': - return `ā†‘ ${networkService.out}`; + return `${networkOutLabel.get()} ${networkService.out}`; default: - return `ā†“ ${networkService.in} ā†‘ ${networkService.out}`; + return `${networkInLabel.get()} ${networkService.in} ${networkOutLabel.get()} ${networkService.out}`; } }; diff --git a/src/components/bar/modules/network/helpers.ts b/src/components/bar/modules/network/helpers.ts index e6e2f19c5..400437e78 100644 --- a/src/components/bar/modules/network/helpers.ts +++ b/src/components/bar/modules/network/helpers.ts @@ -5,8 +5,8 @@ import { networkService } from 'src/lib/constants/services'; export const wiredIcon: Variable = Variable(''); export const wirelessIcon: Variable = Variable(''); -let wiredIconBinding: Variable; -let wirelessIconBinding: Variable; +let wiredIconBinding: Variable | undefined; +let wirelessIconBinding: Variable | undefined; /** * Handles the wired network icon binding. @@ -15,10 +15,8 @@ let wirelessIconBinding: Variable; * then checks if the wired network service is available. If available, it binds the icon name to the `wiredIcon` variable. */ const handleWiredIcon = (): void => { - if (wiredIconBinding) { - wiredIconBinding(); - wiredIconBinding.drop(); - } + wiredIconBinding?.drop(); + wiredIconBinding = undefined; if (!networkService.wired) { return; @@ -36,10 +34,8 @@ const handleWiredIcon = (): void => { * then checks if the wireless network service is available. If available, it binds the icon name to the `wirelessIcon` variable. */ const handleWirelessIcon = (): void => { - if (wirelessIconBinding) { - wirelessIconBinding(); - wirelessIconBinding.drop(); - } + wirelessIconBinding?.drop(); + wirelessIconBinding = undefined; if (!networkService.wifi) { return; diff --git a/src/components/bar/modules/systray/index.tsx b/src/components/bar/modules/systray/index.tsx index ad4727734..2343f6ddd 100644 --- a/src/components/bar/modules/systray/index.tsx +++ b/src/components/bar/modules/systray/index.tsx @@ -10,7 +10,7 @@ const systemtray = AstalTray.get_default(); const { ignore, customIcons } = options.bar.systray; //TODO: Connect to `notify::menu-model` and `notify::action-group` to have up to date menu and action group -const createMenu = (menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup): Gtk.Menu => { +const createMenu = (menuModel: Gio.MenuModel, actionGroup: Gio.ActionGroup | null): Gtk.Menu => { const menu = Gtk.Menu.new_from_model(menuModel); menu.insert_action_group('dbusmenu', actionGroup); @@ -38,9 +38,14 @@ const MenuEntry = ({ item, child }: MenuEntryProps): JSX.Element => { const entryBinding = Variable.derive( [bind(item, 'menuModel'), bind(item, 'actionGroup')], (menuModel, actionGroup) => { - if (menuModel && actionGroup) { - menu = createMenu(menuModel, actionGroup); + if (!menuModel) { + return console.error(`Menu Model not found for ${item.id}`); } + if (!actionGroup) { + return console.error(`Action Group not found for ${item.id}`); + } + + menu = createMenu(menuModel, actionGroup); }, ); diff --git a/src/components/bar/modules/updates/index.tsx b/src/components/bar/modules/updates/index.tsx index a9a535e8d..c2460bfa6 100644 --- a/src/components/bar/modules/updates/index.tsx +++ b/src/components/bar/modules/updates/index.tsx @@ -10,6 +10,7 @@ const { updateCommand, label, padZero, + autoHide, pollingInterval, icon, leftClick, @@ -21,6 +22,7 @@ const { const pendingUpdates: Variable = Variable('0'); const postInputUpdater = Variable(true); +const isVis = Variable(!autoHide.get()); const processUpdateCount = (updateCount: string): string => { if (!padZero.get()) return updateCount; @@ -37,17 +39,24 @@ const updatesPoller = new BashPoller( updatesPoller.initialize('updates'); +Variable.derive([bind(autoHide)], (autoHideModule) => { + isVis.set(!autoHideModule || (autoHideModule && parseFloat(pendingUpdates.get()) > 0)); +}); + const updatesIcon = Variable.derive( [bind(icon.pending), bind(icon.updated), bind(pendingUpdates)], (pendingIcon, updatedIcon, pUpdates) => { - return pUpdates === '0' ? updatedIcon : pendingIcon; + isVis.set(!autoHide.get() || (autoHide.get() && parseFloat(pUpdates) > 0)); + return parseFloat(pUpdates) === 0 ? updatedIcon : pendingIcon; }, ); + export const Updates = (): BarBoxChild => { const updatesModule = Module({ textIcon: updatesIcon(), tooltipText: bind(pendingUpdates).as((v) => `${v} updates available`), boxClass: 'updates', + isVis: isVis, label: bind(pendingUpdates), showLabelBinding: bind(label), props: { diff --git a/src/components/bar/modules/window_title/helpers/title.ts b/src/components/bar/modules/window_title/helpers/title.ts index 333b18ab4..4fdb6aea5 100644 --- a/src/components/bar/modules/window_title/helpers/title.ts +++ b/src/components/bar/modules/window_title/helpers/title.ts @@ -8,11 +8,11 @@ import AstalHyprland from 'gi://AstalHyprland?version=0.1'; * This function searches for a matching window title in the predefined `windowTitleMap` based on the class of the provided window. * If a match is found, it returns an object containing the icon and label for the window. If no match is found, it returns a default icon and label. * - * @param windowtitle The window object containing the class information. + * @param client The window object containing the class information. * * @returns An object containing the icon and label for the window. */ -export const getWindowMatch = (windowtitle: AstalHyprland.Client): Record => { +export const getWindowMatch = (client: AstalHyprland.Client): Record => { const windowTitleMap = [ // user provided values ...options.bar.windowtitle.title_map.get(), @@ -119,17 +119,17 @@ export const getWindowMatch = (windowtitle: AstalHyprland.Client): Record RegExp(wt[0]).test(windowtitle?.class.toLowerCase())); + const foundMatch = windowTitleMap.find((wt) => RegExp(wt[0]).test(client?.class.toLowerCase())); if (!foundMatch || foundMatch.length !== 3) { return { @@ -157,13 +157,12 @@ export const getWindowMatch = (windowtitle: AstalHyprland.Client): Record { - if (client === null) return getWindowMatch(client).label; - - if (useCustomTitle) return getWindowMatch(client).label; - if (useClassName) return client.class; + if (client === null || useCustomTitle) return getWindowMatch(client).label; const title = client.title; - // If the title is empty or only filled with spaces, fallback to the class name + + if (!title || useClassName) return client.class; + if (title.length === 0 || title.match(/^ *$/)) { return client.class; } diff --git a/src/components/bar/modules/workspaces/helpers/index.ts b/src/components/bar/modules/workspaces/helpers/index.ts index eb12b6949..fd20d2cb9 100644 --- a/src/components/bar/modules/workspaces/helpers/index.ts +++ b/src/components/bar/modules/workspaces/helpers/index.ts @@ -33,9 +33,13 @@ export const getWorkspacesForMonitor = ( const monitorMap: MonitorMap = {}; - const workspaceMonitorList = workspaceList.map((m) => { - return { id: m.monitor.id, name: m.monitor.name }; - }); + const wsList = workspaceList ?? []; + + const workspaceMonitorList = wsList + .filter((m) => m !== null) + .map((m) => { + return { id: m.monitor?.id, name: m.monitor?.name }; + }); const monitors = [...new Map([...workspaceMonitorList, ...monitorList].map((item) => [item.id, item])).values()]; @@ -150,7 +154,7 @@ const navigateWorkspace = ( if (workspacesList.length === 0) return; - const currentIndex = workspacesList.indexOf(hyprlandService.focusedWorkspace.id); + const currentIndex = workspacesList.indexOf(hyprlandService.focusedWorkspace?.id); const step = direction === 'next' ? 1 : -1; let newIndex = (currentIndex + step + workspacesList.length) % workspacesList.length; let attempts = 0; @@ -158,7 +162,7 @@ const navigateWorkspace = ( while (attempts < workspacesList.length) { const targetWS = workspacesList[newIndex]; if (!isWorkspaceIgnored(ignoredWorkspaces, targetWS)) { - hyprlandService.message_async(`dispatch workspace ${targetWS}`); + hyprlandService.dispatch('workspace', targetWS.toString()); return; } newIndex = (newIndex + step + workspacesList.length) % workspacesList.length; @@ -283,7 +287,8 @@ export const getWorkspacesToRender = ( let allWorkspaces = range(totalWorkspaces || 8); const activeWorkspaces = workspaceList.map((ws) => ws.id); - const workspaceMonitorList = workspaceList.map((ws) => { + const wsList = workspaceList ?? []; + const workspaceMonitorList = wsList.map((ws) => { return { id: ws.monitor?.id || -1, name: ws.monitor?.name || '', @@ -310,7 +315,7 @@ export const getWorkspacesToRender = ( if (isMonitorSpecific) { const workspacesInRange = range(totalWorkspaces).filter((ws) => { - return getWorkspacesForMonitor(ws, workspaceRules, monitor, workspaceList, monitorList); + return getWorkspacesForMonitor(ws, workspaceRules, monitor, wsList, monitorList); }); allWorkspaces = [...new Set([...activesForMonitor, ...workspacesInRange])]; diff --git a/src/components/bar/modules/workspaces/helpers/utils.ts b/src/components/bar/modules/workspaces/helpers/utils.ts index 5f6ab5d26..955537c98 100644 --- a/src/components/bar/modules/workspaces/helpers/utils.ts +++ b/src/components/bar/modules/workspaces/helpers/utils.ts @@ -21,7 +21,7 @@ const { showWsIcons, showAllActive, numbered_active_indicator: wsActiveIndicator * @returns True if the workspace is active on the monitor, false otherwise. */ const isWorkspaceActiveOnMonitor = (monitor: number, i: number): boolean => { - return showAllActive.get() && hyprlandService.get_monitor(monitor).activeWorkspace.id === i; + return showAllActive.get() && hyprlandService.get_monitor(monitor)?.activeWorkspace?.id === i; }; /** @@ -84,7 +84,7 @@ export const getWsColor = ( showWsIcons.get() && smartHighlight && wsActiveIndicator.get() === 'highlight' && - (hyprlandService.focusedWorkspace.id === i || isWorkspaceActiveOnMonitor(monitor, i)) + (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(); @@ -122,7 +122,7 @@ export const getAppIcon = ( const clients = hyprlandService .get_clients() - .filter((client) => client.workspace.id === workspaceIndex) + .filter((client) => client?.workspace?.id === workspaceIndex) .map((client) => [client.class, client.title]); if (!clients.length) { @@ -200,7 +200,7 @@ export const renderClassnames = ( if (showNumbered || showWsIcons) { const numActiveInd = - hyprlandService.focusedWorkspace.id === i || isWorkspaceActiveOnMonitor(monitor, i) + hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i) ? numberedActiveIndicator : ''; @@ -255,10 +255,10 @@ export const renderLabel = ( } if (showIcons) { - if (hyprlandService.focusedWorkspace.id === i || isWorkspaceActiveOnMonitor(monitor, i)) { + if (hyprlandService.focusedWorkspace?.id === i || isWorkspaceActiveOnMonitor(monitor, i)) { return activeIndicator; } - if ((hyprlandService.get_workspace(i)?.clients.length || 0) > 0) { + if ((hyprlandService.get_workspace(i)?.get_clients().length || 0) > 0) { return occupiedIndicator; } if (monitor !== -1) { diff --git a/src/components/bar/modules/workspaces/index.tsx b/src/components/bar/modules/workspaces/index.tsx index 6940d8a86..eeb7196df 100644 --- a/src/components/bar/modules/workspaces/index.tsx +++ b/src/components/bar/modules/workspaces/index.tsx @@ -1,10 +1,11 @@ import options from 'src/options'; import { createThrottledScrollHandlers, getCurrentMonitorWorkspaces } from './helpers'; -import { BarBoxChild, SelfButton } from 'src/lib/types/bar'; +import { BarBoxChild } from 'src/lib/types/bar'; import { WorkspaceModule } from './workspaces'; import { bind, Variable } from 'astal'; import { GtkWidget } from 'src/lib/types/widget'; -import { Gdk } from 'astal/gtk3'; +import { Astal, Gdk } from 'astal/gtk3'; +import { isScrollDown, isScrollUp } from 'src/lib/utils'; const { workspaces, scroll_speed } = options.bar.workspaces; @@ -27,23 +28,27 @@ const Workspaces = (monitor = -1): BarBoxChild => { boxClass: 'workspaces', isBox: true, props: { - setup: (self: SelfButton): void => { + setup: (self: Astal.EventBox): void => { + let scrollHandlers: number; Variable.derive([bind(scroll_speed)], (scroll_speed) => { + if (scrollHandlers) { + self.disconnect(scrollHandlers); + } + const { throttledScrollUp, throttledScrollDown } = createThrottledScrollHandlers( scroll_speed, currentMonitorWorkspaces, ); - const scrollHandlers = self.connect('scroll-event', (_: GtkWidget, event: Gdk.Event) => { - const eventDirection = event.get_scroll_direction()[1]; - if (eventDirection === Gdk.ScrollDirection.UP) { - throttledScrollUp(); - } else if (eventDirection === Gdk.ScrollDirection.DOWN) { + scrollHandlers = self.connect('scroll-event', (_: GtkWidget, event: Gdk.Event) => { + if (isScrollUp(event)) { throttledScrollDown(); } - }); - self.disconnect(scrollHandlers); + if (isScrollDown(event)) { + throttledScrollUp(); + } + }); }); }, }, diff --git a/src/components/bar/modules/workspaces/workspaces.tsx b/src/components/bar/modules/workspaces/workspaces.tsx index 61914e32f..fa3f924cc 100644 --- a/src/components/bar/modules/workspaces/workspaces.tsx +++ b/src/components/bar/modules/workspaces/workspaces.tsx @@ -86,7 +86,7 @@ export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element smartHighlightEnabled: boolean, monitorList: AstalHyprland.Monitor[], ) => { - const activeWorkspace = hyprlandService.focusedWorkspace.id; + const activeWorkspace = hyprlandService.focusedWorkspace?.id || -99999; const workspacesToRender = getWorkspacesToRender( totalWorkspaces, @@ -152,7 +152,7 @@ export const WorkspaceModule = ({ monitor }: WorkspaceModuleProps): JSX.Element self.toggleClassName('active', activeWorkspace === wsId); self.toggleClassName( 'occupied', - (hyprlandService.get_workspace(wsId)?.get_clients().length || 0) > 0, + (hyprlandService.get_workspace(wsId)?.get_clients()?.length || 0) > 0, ); }} /> diff --git a/src/components/bar/settings/config.tsx b/src/components/bar/settings/config.tsx index c3a940a2e..931e46b06 100644 --- a/src/components/bar/settings/config.tsx +++ b/src/components/bar/settings/config.tsx @@ -140,7 +140,10 @@ export const CustomModuleSettings = (): JSX.Element => {