diff --git a/flake.lock b/flake.lock index 9128f055..51575ff2 100644 --- a/flake.lock +++ b/flake.lock @@ -20,6 +20,24 @@ "type": "github" } }, + "ags": { + "inputs": { + "nixpkgs": "nixpkgs_3" + }, + "locked": { + "lastModified": 1715703984, + "narHash": "sha256-0BZkMui6aCqswMCouvp0G90tAxDOxVnxTvG6TDZsDaI=", + "owner": "Aylur", + "repo": "ags", + "rev": "11150225e62462bcd431d1e55185e810190a730a", + "type": "github" + }, + "original": { + "owner": "Aylur", + "repo": "ags", + "type": "github" + } + }, "base16": { "inputs": { "fromYaml": "fromYaml" @@ -159,14 +177,14 @@ }, "disko": { "inputs": { - "nixpkgs": "nixpkgs_3" + "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1716126753, - "narHash": "sha256-fdodsQ2AWreGj4arHk6cKcnqlWrNiLb64eRrHtMZ5cw=", + "lastModified": 1716394172, + "narHash": "sha256-B+pNhV8GFeCj9/MoH+qtGqKbgv6fU4hGaw2+NoYYtB0=", "owner": "nix-community", "repo": "disko", - "rev": "601be8412d2ab72f752448766fe0fb2f00d5c40c", + "rev": "23c63fb09334c3e8958b57e2ddc3870b75b9111d", "type": "github" }, "original": { @@ -433,11 +451,11 @@ ] }, "locked": { - "lastModified": 1715791817, - "narHash": "sha256-J069Uhv/gCMFLX1dSh2f+9ZTM09r1Nv3oUfocCnWKow=", + "lastModified": 1716327911, + "narHash": "sha256-PI+wygItS/TKzi4gEAROvKTUzTx9GT+PGBttS/IOA/Q=", "owner": "hyprwm", "repo": "hyprcursor", - "rev": "7c3aa03dffb53921e583ade3d4ae3f487e390e7e", + "rev": "27ca640abeef2d425b5dbecf804f5eb622cef56d", "type": "github" }, "original": { @@ -462,11 +480,11 @@ ] }, "locked": { - "lastModified": 1715791817, - "narHash": "sha256-J069Uhv/gCMFLX1dSh2f+9ZTM09r1Nv3oUfocCnWKow=", + "lastModified": 1716327911, + "narHash": "sha256-PI+wygItS/TKzi4gEAROvKTUzTx9GT+PGBttS/IOA/Q=", "owner": "hyprwm", "repo": "hyprcursor", - "rev": "7c3aa03dffb53921e583ade3d4ae3f487e390e7e", + "rev": "27ca640abeef2d425b5dbecf804f5eb622cef56d", "type": "github" }, "original": { @@ -480,16 +498,16 @@ "hyprcursor": "hyprcursor", "hyprlang": "hyprlang", "hyprwayland-scanner": "hyprwayland-scanner", - "nixpkgs": "nixpkgs_4", + "nixpkgs": "nixpkgs_5", "systems": "systems_4", "xdph": "xdph" }, "locked": { - "lastModified": 1716063601, - "narHash": "sha256-gAuCKupztnqai1tZ6TyCFCRbeFzbggL0Oe0vl0/cwK8=", + "lastModified": 1716417827, + "narHash": "sha256-TYHpA/i9+Ns01+RzknJ5eYskQXL9GTTA7JX9Lo4JKVg=", "ref": "refs/heads/main", - "rev": "f8857e6072bd85b95393499688872aaf7f088b5b", - "revCount": 4719, + "rev": "7ad9116de8d0b7dac27eaf080bd92998a8fb40e5", + "revCount": 4728, "submodules": true, "type": "git", "url": "https://github.com/hyprwm/Hyprland" @@ -616,11 +634,11 @@ ] }, "locked": { - "lastModified": 1715879663, - "narHash": "sha256-/DwglRvj4XF4ECdNtrCIbthleszAZBwOiXG5A6r0K/c=", + "lastModified": 1716058375, + "narHash": "sha256-CwjWoVnBZE5SBpRx9dgSQGCr4Goxyfcyv3zZbOhVqzk=", "owner": "hyprwm", "repo": "hyprwayland-scanner", - "rev": "f5181a068c1b06f2db51f6222e50a0c665a2b0c3", + "rev": "3afed4364790aebe0426077631af1e164a9650cc", "type": "github" }, "original": { @@ -641,11 +659,11 @@ ] }, "locked": { - "lastModified": 1715879663, - "narHash": "sha256-/DwglRvj4XF4ECdNtrCIbthleszAZBwOiXG5A6r0K/c=", + "lastModified": 1716058375, + "narHash": "sha256-CwjWoVnBZE5SBpRx9dgSQGCr4Goxyfcyv3zZbOhVqzk=", "owner": "hyprwm", "repo": "hyprwayland-scanner", - "rev": "f5181a068c1b06f2db51f6222e50a0c665a2b0c3", + "rev": "3afed4364790aebe0426077631af1e164a9650cc", "type": "github" }, "original": { @@ -654,19 +672,22 @@ "type": "github" } }, - "monolisa-script": { - "flake": false, + "matugen": { + "inputs": { + "nixpkgs": "nixpkgs_6" + }, "locked": { - "lastModified": 1709742524, - "narHash": "sha256-mkw+WhtLw7498iNI4jTdeTq2eteJsfgxN0o8hs0q2l8=", - "owner": "redyf", - "repo": "test2", - "rev": "c234fad704153e0244f160e1e293f35c4b2ff26e", + "lastModified": 1711657889, + "narHash": "sha256-4VX7Rt+ftEH8nwg59eT7TsvHYUf8/euUmwh/JLc4rLc=", + "owner": "InioX", + "repo": "matugen", + "rev": "566277529dadc2b149a8bd8b9859ea791ecdef26", "type": "github" }, "original": { - "owner": "redyf", - "repo": "test2", + "owner": "InioX", + "ref": "v2.2.0", + "repo": "matugen", "type": "github" } }, @@ -724,43 +745,59 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1715774670, - "narHash": "sha256-iJYnKMtLi5u6hZhJm94cRNSDG5Rz6ZzIkGbhPFtDRm0=", + "lastModified": 1708475490, + "narHash": "sha256-g1v0TsWBQPX97ziznfJdWhgMyMGtoBFs102xSYO4syU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b3fcfcfabd01b947a1e4f36622bbffa3985bdac6", + "rev": "0e74ca98a74bc7270d28838369593635a5db3260", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixpkgs-unstable", + "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_4": { "locked": { - "lastModified": 1715787315, - "narHash": "sha256-cYApT0NXJfqBkKcci7D9Kr4CBYZKOQKDYA23q8XNuWg=", + "lastModified": 1716128955, + "narHash": "sha256-3DNg/PV+X2V7yn8b/fUR2ppakw7D9N4sjVBGk6nDwII=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "33d1e753c82ffc557b4a585c77de43d4c922ebb5", + "rev": "f9256de8281f2ccd04985ac5c30d8f69aefadbe8", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-unstable", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "nixpkgs_5": { "locked": { - "lastModified": 1715961556, - "narHash": "sha256-+NpbZRCRisUHKQJZF3CT+xn14ZZQO+KjxIIanH3Pvn4=", + "lastModified": 1716330097, + "narHash": "sha256-8BO3B7e3BiyIDsaKA0tY8O88rClYRTjvAp66y+VBUeU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5710852ba686cc1fd0d3b8e22b3117d43ba374c2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_6": { + "locked": { + "lastModified": 1691186842, + "narHash": "sha256-wxBVCvZUwq+XS4N4t9NqsHV4E64cPVqQ2fdDISpjcw0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4a6b83b05df1a8bd7d99095ec4b4d271f2956b64", + "rev": "18036c0be90f4e308ae3ebcab0e14aae0336fe42", "type": "github" }, "original": { @@ -770,7 +807,23 @@ "type": "github" } }, - "nixpkgs_6": { + "nixpkgs_7": { + "locked": { + "lastModified": 1716330097, + "narHash": "sha256-8BO3B7e3BiyIDsaKA0tY8O88rClYRTjvAp66y+VBUeU=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5710852ba686cc1fd0d3b8e22b3117d43ba374c2", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_8": { "locked": { "lastModified": 1714912032, "narHash": "sha256-clkcOIkg8G4xuJh+1onLG4HPMpbtzdLv4rHxFzgsH9c=", @@ -786,13 +839,13 @@ "type": "github" } }, - "nixpkgs_7": { + "nixpkgs_9": { "locked": { - "lastModified": 1715787315, - "narHash": "sha256-cYApT0NXJfqBkKcci7D9Kr4CBYZKOQKDYA23q8XNuWg=", + "lastModified": 1716330097, + "narHash": "sha256-8BO3B7e3BiyIDsaKA0tY8O88rClYRTjvAp66y+VBUeU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "33d1e753c82ffc557b4a585c77de43d4c922ebb5", + "rev": "5710852ba686cc1fd0d3b8e22b3117d43ba374c2", "type": "github" }, "original": { @@ -828,11 +881,11 @@ }, "nur": { "locked": { - "lastModified": 1716155250, - "narHash": "sha256-hyjUJFrCxYJOIK2xM4IZCnW97HJpnUtv2NyykjoF5tE=", + "lastModified": 1716421554, + "narHash": "sha256-a01RsgodkJnBAg6xrNRBeT2tcrOc9JdEJ6q4MGXyuV0=", "owner": "nix-community", "repo": "NUR", - "rev": "434a49f8736ccc9d108a6d3c34f2650bf15a3fe0", + "rev": "cd2265a92c4cb32b3ae23602dfdbe0a9c5833c6a", "type": "github" }, "original": { @@ -874,11 +927,12 @@ "root": { "inputs": { "Neve": "Neve", + "ags": "ags", "disko": "disko", "home-manager": "home-manager_2", "hyprland": "hyprland", - "monolisa-script": "monolisa-script", - "nixpkgs": "nixpkgs_5", + "matugen": "matugen", + "nixpkgs": "nixpkgs_7", "nur": "nur", "sf-mono-liga-src": "sf-mono-liga-src", "stylix": "stylix", @@ -913,14 +967,14 @@ "flake-compat": "flake-compat_3", "gnome-shell": "gnome-shell", "home-manager": "home-manager_3", - "nixpkgs": "nixpkgs_6" + "nixpkgs": "nixpkgs_8" }, "locked": { - "lastModified": 1716037261, - "narHash": "sha256-eF0A36GdegKkEiwFArjCysGU/XEYvzj7x5jfkFMtmqM=", + "lastModified": 1716395969, + "narHash": "sha256-Qse5s/R8QKdI6yYnDv9pcDSrR8qVWzJ2m1QMjkuVxuU=", "owner": "danth", "repo": "stylix", - "rev": "76e7daf5a16d442ac98e844582f7dc1354610886", + "rev": "e7543c51eff9e73c85450c473e1f24513a5e0a0f", "type": "github" }, "original": { @@ -1009,16 +1063,16 @@ "hyprcursor": "hyprcursor_2", "hyprlang": "hyprlang_2", "hyprwayland-scanner": "hyprwayland-scanner_2", - "nixpkgs": "nixpkgs_7", + "nixpkgs": "nixpkgs_9", "systems": "systems_5", "xdph": "xdph_2" }, "locked": { - "lastModified": 1716063601, - "narHash": "sha256-lH2CLdRQFtbQVauhLFDbPWTGmj7LgblMg2dq9thd0Zc=", + "lastModified": 1716417827, + "narHash": "sha256-2br1N9Nx5adieXKZPKWf8jKovWbLFNDw9/4NJdAhF2w=", "owner": "hyprwm", "repo": "hyprland", - "rev": "f8857e6072bd85b95393499688872aaf7f088b5b", + "rev": "7ad9116de8d0b7dac27eaf080bd92998a8fb40e5", "type": "github" }, "original": { @@ -1044,11 +1098,11 @@ ] }, "locked": { - "lastModified": 1715788457, - "narHash": "sha256-32HOkjSIyANphV0p5gIwP4ONU/CcinhwOyVFB+tL/d0=", + "lastModified": 1716290197, + "narHash": "sha256-1u9Exrc7yx9qtES2brDh7/DDZ8w8ap1nboIOAtCgeuM=", "owner": "hyprwm", "repo": "xdg-desktop-portal-hyprland", - "rev": "af7c87a32f5d67eb2ada908a6a700f4e74831943", + "rev": "91e48d6acd8a5a611d26f925e51559ab743bc438", "type": "github" }, "original": { @@ -1074,11 +1128,11 @@ ] }, "locked": { - "lastModified": 1715788457, - "narHash": "sha256-32HOkjSIyANphV0p5gIwP4ONU/CcinhwOyVFB+tL/d0=", + "lastModified": 1716290197, + "narHash": "sha256-1u9Exrc7yx9qtES2brDh7/DDZ8w8ap1nboIOAtCgeuM=", "owner": "hyprwm", "repo": "xdg-desktop-portal-hyprland", - "rev": "af7c87a32f5d67eb2ada908a6a700f4e74831943", + "rev": "91e48d6acd8a5a611d26f925e51559ab743bc438", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 705d0f3c..da248539 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,8 @@ Neve.url = "github:redyf/Neve"; disko.url = "github:nix-community/disko"; stylix.url = "github:danth/stylix"; + ags.url = "github:Aylur/ags"; + matugen.url = "github:InioX/matugen?ref=v2.2.0"; # SFMono w/ patches sf-mono-liga-src = { @@ -22,10 +24,16 @@ flake = false; }; - monolisa-script = { - url = "github:redyf/test2"; - flake = false; - }; + # git+ssh://git@git.example.com/User/repo.git + # berkeley = { + # url = "git+ssh://git@github.com/redyf/berkeley.git"; + # flake = false; + # }; + + # monolisa-script = { + # url = "git+ssh://git@github.com/redyf/test2.git"; + # flake = false; + # }; }; outputs = { diff --git a/home/redyf/apps/misc/default.nix b/home/redyf/apps/misc/default.nix index 90d1da25..fd0b044f 100644 --- a/home/redyf/apps/misc/default.nix +++ b/home/redyf/apps/misc/default.nix @@ -21,9 +21,10 @@ gh ollama playerctl + # spotify # Rice - mako + # mako bemenu # cmatrix nitrogen # Wallpaper utility for X11 diff --git a/home/redyf/cli-apps/tmux/rose-pine.conf b/home/redyf/cli-apps/tmux/rose-pine.conf index 2d8b5cde..0a3c12f8 100644 --- a/home/redyf/cli-apps/tmux/rose-pine.conf +++ b/home/redyf/cli-apps/tmux/rose-pine.conf @@ -8,7 +8,7 @@ set -g @rose_pine_bar_bg_disable "on" # Disables background color, for transpare # If @rose_pine_bar_bg_disable is set to "on", uses the provided value to set the background color # It can be any of the on tmux (named colors, 256-color set, `default` or hex colors) # See more on http://man.openbsd.org/OpenBSD-current/man1/tmux.1#STYLES -set -g @rose_pine_bar_bg_disabled_color_option "#232136" +set -g @rose_pine_bar_bg_disabled_color_option "default" set -g @rose_pine_only_windows "on" # Leaves only the window module, for max focus and space set -g @rose_pine_disable_active_window_menu "on" # Disables the menu that shows the active window on the left diff --git a/home/redyf/desktop/addons/ags/config/.eslintrc.yml b/home/redyf/desktop/addons/ags/config/.eslintrc.yml new file mode 100644 index 00000000..ff96a835 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/.eslintrc.yml @@ -0,0 +1,130 @@ +env: + es2022: true +extends: + - "eslint:recommended" + - "plugin:@typescript-eslint/recommended" +parser: "@typescript-eslint/parser" +parserOptions: + ecmaVersion: 2022 + sourceType: "module" + project: "./tsconfig.json" + warnOnUnsupportedTypeScriptVersion: false +root: true +ignorePatterns: + - types/ +plugins: + - "@typescript-eslint" +rules: + "@typescript-eslint/ban-ts-comment": + - "off" + "@typescript-eslint/no-non-null-assertion": + - "off" + # "@typescript-eslint/no-explicit-any": + # - "off" + "@typescript-eslint/no-unused-vars": + - error + - varsIgnorePattern: (^unused|_$) + argsIgnorePattern: ^(unused|_) + "@typescript-eslint/no-empty-interface": + - "off" + + arrow-parens: + - error + - as-needed + comma-dangle: + - error + - always-multiline + comma-spacing: + - error + - before: false + after: true + comma-style: + - error + - last + curly: + - error + - multi-or-nest + - consistent + dot-location: + - error + - property + eol-last: + - error + eqeqeq: + - error + - always + indent: + - error + - 4 + - SwitchCase: 1 + keyword-spacing: + - error + - before: true + lines-between-class-members: + - error + - always + - exceptAfterSingleLine: true + padded-blocks: + - error + - never + - allowSingleLineBlocks: false + prefer-const: + - error + quotes: + - error + - double + - avoidEscape: true + semi: + - error + - never + nonblock-statement-body-position: + - error + - below + no-trailing-spaces: + - error + no-useless-escape: + - off + max-len: + - error + - code: 100 + func-call-spacing: + - error + array-bracket-spacing: + - error + space-before-function-paren: + - error + - anonymous: never + named: never + asyncArrow: ignore + space-before-blocks: + - error + key-spacing: + - error + object-curly-spacing: + - error + - always +globals: + Widget: readonly + Utils: readonly + App: readonly + Variable: readonly + Service: readonly + pkg: readonly + ARGV: readonly + Debugger: readonly + GIRepositoryGType: readonly + globalThis: readonly + imports: readonly + Intl: readonly + log: readonly + logError: readonly + print: readonly + printerr: readonly + window: readonly + TextEncoder: readonly + TextDecoder: readonly + console: readonly + setTimeout: readonly + setInterval: readonly + clearTimeout: readonly + clearInterval: readonly diff --git a/home/redyf/desktop/addons/ags/config/.gitignore b/home/redyf/desktop/addons/ags/config/.gitignore new file mode 100644 index 00000000..f56dbd18 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/.gitignore @@ -0,0 +1,6 @@ +node_modules +types +package-lock.json +bun.lockb +flake.lock +.weather diff --git a/home/redyf/desktop/addons/ags/config/assets/battery-flash-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/battery-flash-symbolic.svg new file mode 100644 index 00000000..21b5e332 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/battery-flash-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/chat-bubbles-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/chat-bubbles-symbolic.svg new file mode 100644 index 00000000..fdee0b38 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/chat-bubbles-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/controller-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/controller-symbolic.svg new file mode 100644 index 00000000..98bf5d62 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/controller-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/controls-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/controls-symbolic.svg new file mode 100644 index 00000000..7df5663f --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/controls-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/dark-mode-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/dark-mode-symbolic.svg new file mode 100644 index 00000000..9f2e6b4e --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/dark-mode-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/hourglass-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/hourglass-symbolic.svg new file mode 100644 index 00000000..aa4f97cf --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/hourglass-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/light-mode-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/light-mode-symbolic.svg new file mode 100644 index 00000000..d5fb2713 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/light-mode-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/mixer-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/mixer-symbolic.svg new file mode 100644 index 00000000..ad6cfa85 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/mixer-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/nix-snowflake-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/nix-snowflake-symbolic.svg new file mode 100644 index 00000000..7bb42edd --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/nix-snowflake-symbolic.svg @@ -0,0 +1,155 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/preferences-desktop-theme-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/preferences-desktop-theme-symbolic.svg new file mode 100644 index 00000000..44614542 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/preferences-desktop-theme-symbolic.svg @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/processor-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/processor-symbolic.svg new file mode 100644 index 00000000..832dbaf3 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/processor-symbolic.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/terminal-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/terminal-symbolic.svg new file mode 100644 index 00000000..9f82bcf5 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/terminal-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/home/redyf/desktop/addons/ags/config/assets/toolbars-symbolic.svg b/home/redyf/desktop/addons/ags/config/assets/toolbars-symbolic.svg new file mode 100644 index 00000000..9f4c564a --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/assets/toolbars-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/home/redyf/desktop/addons/ags/config/config.js b/home/redyf/desktop/addons/ags/config/config.js new file mode 100644 index 00000000..72b6c8aa --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/config.js @@ -0,0 +1,46 @@ +import GLib from "gi://GLib" + +const main = "/tmp/asztal/main.js" +const entry = `${App.configDir}/main.ts` +const bundler = GLib.getenv("AGS_BUNDLER") || "bun" + +const v = { + ags: pkg.version?.split(".").map(Number) || [], + expect: [1, 8, 1], +} + +try { + switch (bundler) { + case "bun": await Utils.execAsync([ + "bun", "build", entry, + "--outfile", main, + "--external", "resource://*", + "--external", "gi://*", + "--external", "file://*", + ]); break + + case "esbuild": await Utils.execAsync([ + "esbuild", "--bundle", entry, + "--format=esm", + `--outfile=${main}`, + "--external:resource://*", + "--external:gi://*", + "--external:file://*", + ]); break + + default: + throw `"${bundler}" is not a valid bundler` + } + + if (v.ags[1] < v.expect[1] || v.ags[2] < v.expect[2]) { + print(`my config needs at least v${v.expect.join(".")}, yours is v${v.ags.join(".")}`) + App.quit() + } + + await import(`file://${main}`) +} catch (error) { + console.error(error) + App.quit() +} + +export { } diff --git a/home/redyf/desktop/addons/ags/config/default.nix b/home/redyf/desktop/addons/ags/config/default.nix new file mode 100644 index 00000000..ad2b77f1 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/default.nix @@ -0,0 +1,103 @@ +{ + inputs, + writeShellScript, + system, + stdenv, + cage, + swww, + esbuild, + dart-sass, + fd, + fzf, + brightnessctl, + accountsservice, + slurp, + wf-recorder, + wl-clipboard, + wayshot, + swappy, + hyprpicker, + pavucontrol, + networkmanager, + gtk3, + which, +}: let + name = "asztal"; + + ags = inputs.ags.packages.${system}.default.override { + extraPackages = [accountsservice]; + }; + + dependencies = [ + which + dart-sass + fd + fzf + brightnessctl + swww + inputs.matugen.packages.${system}.default + slurp + wf-recorder + wl-clipboard + wayshot + swappy + hyprpicker + pavucontrol + networkmanager + gtk3 + ]; + + addBins = list: builtins.concatStringsSep ":" (builtins.map (p: "${p}/bin") list); + + greeter = writeShellScript "greeter" '' + export PATH=$PATH:${addBins dependencies} + ${cage}/bin/cage -ds -m last ${ags}/bin/ags -- -c ${config}/greeter.js + ''; + + desktop = writeShellScript name '' + export PATH=$PATH:${addBins dependencies} + ${ags}/bin/ags -b ${name} -c ${config}/config.js $@ + ''; + + config = stdenv.mkDerivation { + inherit name; + src = ./.; + + buildPhase = '' + ${esbuild}/bin/esbuild \ + --bundle ./main.ts \ + --outfile=main.js \ + --format=esm \ + --external:resource://\* \ + --external:gi://\* \ + + ${esbuild}/bin/esbuild \ + --bundle ./greeter/greeter.ts \ + --outfile=greeter.js \ + --format=esm \ + --external:resource://\* \ + --external:gi://\* \ + ''; + + installPhase = '' + mkdir -p $out + cp -r assets $out + cp -r style $out + cp -r greeter $out + cp -r widget $out + cp -f main.js $out/config.js + cp -f greeter.js $out/greeter.js + ''; + }; +in + stdenv.mkDerivation { + inherit name; + src = config; + + installPhase = '' + mkdir -p $out/bin + cp -r . $out + cp ${desktop} $out/bin/${name} + cp ${greeter} $out/bin/greeter + ''; + } diff --git a/home/redyf/desktop/addons/ags/config/greeter.js b/home/redyf/desktop/addons/ags/config/greeter.js new file mode 100644 index 00000000..5c8e3690 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/greeter.js @@ -0,0 +1,18 @@ +const main = "/tmp/ags/greeter.js" +const entry = `${App.configDir}/greeter/greeter.ts` + +try { + await Utils.execAsync([ + "bun", "build", entry, + "--outfile", main, + "--external", "resource://*", + "--external", "gi://*", + "--external", "file://*", + ]) + await import(`file://${main}`) +} catch (error) { + console.error(error) + App.quit() +} + +export { } diff --git a/home/redyf/desktop/addons/ags/config/greeter/auth.ts b/home/redyf/desktop/addons/ags/config/greeter/auth.ts new file mode 100644 index 00000000..23477ebc --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/greeter/auth.ts @@ -0,0 +1,109 @@ +import AccountsService from "gi://AccountsService?version=1.0" +import GLib from "gi://GLib?version=2.0" +import icons from "lib/icons" + +const { iconFile, realName, userName } = AccountsService.UserManager + .get_default().list_users()[0] + +const loggingin = Variable(false) + +const CMD = GLib.getenv("ASZTAL_DM_CMD") + || "Hyprland" + +const ENV = GLib.getenv("ASZTAL_DM_ENV") + || "WLR_NO_HARDWARE_CURSORS=1 _JAVA_AWT_WM_NONREPARENTING=1" + +async function login(pw: string) { + loggingin.value = true + const greetd = await Service.import("greetd") + return greetd.login(userName, pw, CMD, ENV.split(/\s+/)) + .catch(res => { + loggingin.value = false + response.label = res?.description || JSON.stringify(res) + password.text = "" + revealer.reveal_child = true + }) +} + +const avatar = Widget.Box({ + class_name: "avatar", + hpack: "center", + css: `background-image: url('${iconFile}')`, +}) + +const password = Widget.Entry({ + placeholder_text: "Password", + hexpand: true, + visibility: false, + on_accept: ({ text }) => { login(text || "") }, +}) + +const response = Widget.Label({ + class_name: "response", + wrap: true, + max_width_chars: 35, + hpack: "center", + hexpand: true, + xalign: .5, +}) + +const revealer = Widget.Revealer({ + transition: "slide_down", + child: response, +}) + +export default Widget.Box({ + class_name: "auth", + attribute: { password }, + vertical: true, + children: [ + Widget.Overlay({ + child: Widget.Box( + { + css: "min-width: 200px; min-height: 200px;", + vertical: true, + }, + Widget.Box({ + class_name: "wallpaper", + css: `background-image: url('${WALLPAPER}')`, + }), + Widget.Box({ + class_name: "wallpaper-contrast", + vexpand: true, + }), + ), + overlay: Widget.Box( + { + vpack: "end", + vertical: true, + }, + avatar, + Widget.Box({ + hpack: "center", + children: [ + Widget.Icon(icons.ui.avatar), + Widget.Label(realName || userName), + ], + }), + Widget.Box( + { + class_name: "password", + }, + Widget.Spinner({ + visible: loggingin.bind(), + active: true, + }), + Widget.Icon({ + visible: loggingin.bind().as(b => !b), + icon: icons.ui.lock, + }), + password, + ), + ), + }), + Widget.Box( + { class_name: "response-box" }, + revealer, + ), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/greeter/greeter.ts b/home/redyf/desktop/addons/ags/config/greeter/greeter.ts new file mode 100644 index 00000000..eb1493f9 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/greeter/greeter.ts @@ -0,0 +1,37 @@ +import "./session" +import "style/style" +import GLib from "gi://GLib?version=2.0" +import RegularWindow from "widget/RegularWindow" +import statusbar from "./statusbar" +import auth from "./auth" + +const win = RegularWindow({ + name: "greeter", + setup: self => { + self.set_default_size(500, 500) + self.show_all() + auth.attribute.password.grab_focus() + }, + child: Widget.Overlay({ + child: Widget.Box({ expand: true }), + overlays: [ + Widget.Box({ + vpack: "start", + hpack: "fill", + hexpand: true, + child: statusbar, + }), + Widget.Box({ + vpack: "center", + hpack: "center", + child: auth, + }), + ], + }), +}) + +App.config({ + icons: "./assets", + windows: [win], + cursorTheme: GLib.getenv("XCURSOR_THEME")!, +}) diff --git a/home/redyf/desktop/addons/ags/config/greeter/session.ts b/home/redyf/desktop/addons/ags/config/greeter/session.ts new file mode 100644 index 00000000..092a5c27 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/greeter/session.ts @@ -0,0 +1,20 @@ +import GLib from "gi://GLib?version=2.0" +import AccountsService from "gi://AccountsService?version=1.0" + +const { userName } = AccountsService.UserManager.get_default().list_users()[0] + +declare global { + const WALLPAPER: string +} + +Object.assign(globalThis, { + TMP: `${GLib.get_tmp_dir()}/greeter`, + OPTIONS: "/var/cache/greeter/options.json", + WALLPAPER: "/var/cache/greeter/background", + // TMP: "/tmp/ags", + // OPTIONS: Utils.CACHE_DIR + "/options.json", + // WALLPAPER: Utils.HOME + "/.config/background", + USER: userName, +}) + +Utils.ensureDirectory(TMP) diff --git a/home/redyf/desktop/addons/ags/config/greeter/statusbar.ts b/home/redyf/desktop/addons/ags/config/greeter/statusbar.ts new file mode 100644 index 00000000..8076011b --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/greeter/statusbar.ts @@ -0,0 +1,46 @@ +import { clock } from "lib/variables" +import options from "options" +import icons from "lib/icons" +import BatteryBar from "widget/bar/buttons/BatteryBar" +import PanelButton from "widget/bar/PanelButton" + +const { scheme } = options.theme +const { monochrome } = options.bar.powermenu +const { format } = options.bar.date + +const poweroff = PanelButton({ + class_name: "powermenu", + child: Widget.Icon(icons.powermenu.shutdown), + on_clicked: () => Utils.exec("shutdown now"), + setup: self => self.hook(monochrome, () => { + self.toggleClassName("colored", !monochrome.value) + self.toggleClassName("box") + }), +}) + +const date = PanelButton({ + class_name: "date", + child: Widget.Label({ + label: clock.bind().as(c => c.format(`${format}`)!), + }), +}) + +const darkmode = PanelButton({ + class_name: "darkmode", + child: Widget.Icon({ icon: scheme.bind().as(s => icons.color[s]) }), + on_clicked: () => scheme.value = scheme.value === "dark" ? "light" : "dark", +}) + +export default Widget.CenterBox({ + class_name: "bar", + hexpand: true, + center_widget: date, + end_widget: Widget.Box({ + hpack: "end", + children: [ + darkmode, + BatteryBar(), + poweroff, + ], + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/lib/battery.ts b/home/redyf/desktop/addons/ags/config/lib/battery.ts new file mode 100644 index 00000000..38172602 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/battery.ts @@ -0,0 +1,16 @@ +import icons from "./icons" + +export default async function init() { + const bat = await Service.import("battery") + bat.connect("notify::percent", ({ percent, charging }) => { + const low = 30 + if (percent !== low || percent !== low / 2 || !charging) + return + + Utils.notify({ + summary: `${percent}% Battery Percentage`, + iconName: icons.battery.warning, + urgency: "critical", + }) + }) +} diff --git a/home/redyf/desktop/addons/ags/config/lib/gtk.ts b/home/redyf/desktop/addons/ags/config/lib/gtk.ts new file mode 100644 index 00000000..8cd60a3b --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/gtk.ts @@ -0,0 +1,16 @@ +import Gio from "gi://Gio" +import options from "options" + +const settings = new Gio.Settings({ + schema: "org.gnome.desktop.interface", +}) + +function gtk() { + const scheme = options.theme.scheme.value + settings.set_string("color-scheme", `prefer-${scheme}`) +} + +export default function init() { + options.theme.scheme.connect("changed", gtk) + gtk() +} diff --git a/home/redyf/desktop/addons/ags/config/lib/hyprland.ts b/home/redyf/desktop/addons/ags/config/lib/hyprland.ts new file mode 100644 index 00000000..95a6d143 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/hyprland.ts @@ -0,0 +1,82 @@ +import options from "options" +const { messageAsync } = await Service.import("hyprland") + +const { + hyprland, + theme: { + spacing, + radius, + border: { width }, + blur, + shadows, + dark: { + primary: { bg: darkActive }, + }, + light: { + primary: { bg: lightActive }, + }, + scheme, + }, +} = options + +const deps = [ + "hyprland", + spacing.id, + radius.id, + blur.id, + width.id, + shadows.id, + darkActive.id, + lightActive.id, + scheme.id, +] + +function primary() { + return scheme.value === "dark" + ? darkActive.value + : lightActive.value +} + +function rgba(color: string) { + return `rgba(${color}ff)`.replace("#", "") +} + +function sendBatch(batch: string[]) { + const cmd = batch + .filter(x => !!x) + .map(x => `keyword ${x}`) + .join("; ") + + return messageAsync(`[[BATCH]]/${cmd}`) +} + +async function setupHyprland() { + const wm_gaps = Math.floor(hyprland.gaps.value * spacing.value) + + sendBatch([ + `general:border_size ${width}`, + `general:gaps_out ${wm_gaps}`, + `general:gaps_in ${Math.floor(wm_gaps / 2)}`, + `general:col.active_border ${rgba(primary())}`, + `general:col.inactive_border ${rgba(hyprland.inactiveBorder.value)}`, + `decoration:rounding ${radius}`, + `decoration:drop_shadow ${shadows.value ? "yes" : "no"}`, + `dwindle:no_gaps_when_only ${hyprland.gapsWhenOnly.value ? 0 : 1}`, + `master:no_gaps_when_only ${hyprland.gapsWhenOnly.value ? 0 : 1}`, + ]) + + await sendBatch(App.windows.map(({ name }) => `layerrule unset, ${name}`)) + + if (blur.value > 0) { + sendBatch(App.windows.flatMap(({ name }) => [ + `layerrule unset, ${name}`, + `layerrule blur, ${name}`, + `layerrule ignorealpha ${/* based on shadow color */.29}, ${name}`, + ])) + } +} + +export default function init() { + options.handler(deps, setupHyprland) + setupHyprland() +} diff --git a/home/redyf/desktop/addons/ags/config/lib/icons.ts b/home/redyf/desktop/addons/ags/config/lib/icons.ts new file mode 100644 index 00000000..f6da6973 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/icons.ts @@ -0,0 +1,145 @@ +export const substitutes = { + "transmission-gtk": "transmission", + "blueberry.py": "blueberry", + "Caprine": "facebook-messenger", + "com.raggesilver.BlackBox-symbolic": "terminal-symbolic", + "org.wezfurlong.wezterm-symbolic": "terminal-symbolic", + "audio-headset-bluetooth": "audio-headphones-symbolic", + "audio-card-analog-usb": "audio-speakers-symbolic", + "audio-card-analog-pci": "audio-card-symbolic", + "preferences-system": "emblem-system-symbolic", + "com.github.Aylur.ags-symbolic": "controls-symbolic", + "com.github.Aylur.ags": "controls-symbolic", +} + +export default { + missing: "image-missing-symbolic", + nix: { + nix: "nix-snowflake-symbolic", + }, + app: { + terminal: "terminal-symbolic", + }, + fallback: { + executable: "application-x-executable", + notification: "dialog-information-symbolic", + video: "video-x-generic-symbolic", + audio: "audio-x-generic-symbolic", + }, + ui: { + close: "window-close-symbolic", + colorpicker: "color-select-symbolic", + info: "info-symbolic", + link: "external-link-symbolic", + lock: "system-lock-screen-symbolic", + menu: "open-menu-symbolic", + refresh: "view-refresh-symbolic", + search: "system-search-symbolic", + settings: "emblem-system-symbolic", + themes: "preferences-desktop-theme-symbolic", + tick: "object-select-symbolic", + time: "hourglass-symbolic", + toolbars: "toolbars-symbolic", + warning: "dialog-warning-symbolic", + avatar: "avatar-default-symbolic", + arrow: { + right: "pan-end-symbolic", + left: "pan-start-symbolic", + down: "pan-down-symbolic", + up: "pan-up-symbolic", + }, + }, + audio: { + mic: { + muted: "microphone-disabled-symbolic", + low: "microphone-sensitivity-low-symbolic", + medium: "microphone-sensitivity-medium-symbolic", + high: "microphone-sensitivity-high-symbolic", + }, + volume: { + muted: "audio-volume-muted-symbolic", + low: "audio-volume-low-symbolic", + medium: "audio-volume-medium-symbolic", + high: "audio-volume-high-symbolic", + overamplified: "audio-volume-overamplified-symbolic", + }, + type: { + headset: "audio-headphones-symbolic", + speaker: "audio-speakers-symbolic", + card: "audio-card-symbolic", + }, + mixer: "mixer-symbolic", + }, + powerprofile: { + balanced: "power-profile-balanced-symbolic", + "power-saver": "power-profile-power-saver-symbolic", + performance: "power-profile-performance-symbolic", + }, + asusctl: { + profile: { + Balanced: "power-profile-balanced-symbolic", + Quiet: "power-profile-power-saver-symbolic", + Performance: "power-profile-performance-symbolic", + }, + mode: { + Integrated: "processor-symbolic", + Hybrid: "controller-symbolic", + }, + }, + battery: { + charging: "battery-flash-symbolic", + warning: "battery-empty-symbolic", + }, + bluetooth: { + enabled: "bluetooth-active-symbolic", + disabled: "bluetooth-disabled-symbolic", + }, + brightness: { + indicator: "display-brightness-symbolic", + keyboard: "keyboard-brightness-symbolic", + screen: "display-brightness-symbolic", + }, + powermenu: { + sleep: "weather-clear-night-symbolic", + reboot: "system-reboot-symbolic", + logout: "system-log-out-symbolic", + shutdown: "system-shutdown-symbolic", + }, + recorder: { + recording: "media-record-symbolic", + }, + notifications: { + noisy: "org.gnome.Settings-notifications-symbolic", + silent: "notifications-disabled-symbolic", + message: "chat-bubbles-symbolic", + }, + trash: { + full: "user-trash-full-symbolic", + empty: "user-trash-symbolic", + }, + mpris: { + shuffle: { + enabled: "media-playlist-shuffle-symbolic", + disabled: "media-playlist-consecutive-symbolic", + }, + loop: { + none: "media-playlist-repeat-symbolic", + track: "media-playlist-repeat-song-symbolic", + playlist: "media-playlist-repeat-symbolic", + }, + playing: "media-playback-pause-symbolic", + paused: "media-playback-start-symbolic", + stopped: "media-playback-start-symbolic", + prev: "media-skip-backward-symbolic", + next: "media-skip-forward-symbolic", + }, + system: { + cpu: "org.gnome.SystemMonitor-symbolic", + ram: "drive-harddisk-solidstate-symbolic", + temp: "temperature-symbolic", + }, + color: { + dark: "dark-mode-symbolic", + light: "light-mode-symbolic", + }, +} diff --git a/home/redyf/desktop/addons/ags/config/lib/init.ts b/home/redyf/desktop/addons/ags/config/lib/init.ts new file mode 100644 index 00000000..e19385f6 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/init.ts @@ -0,0 +1,19 @@ +import matugen from "./matugen"; +import hyprland from "./hyprland"; +import tmux from "./tmux"; +import gtk from "./gtk"; +import lowBattery from "./battery"; +import notifications from "./notifications"; + +export default function init() { + try { + gtk(); + tmux(); + matugen(); + lowBattery(); + notifications(); + // hyprland(); + } catch (error) { + logError(error); + } +} diff --git a/home/redyf/desktop/addons/ags/config/lib/matugen.ts b/home/redyf/desktop/addons/ags/config/lib/matugen.ts new file mode 100644 index 00000000..dfccccfb --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/matugen.ts @@ -0,0 +1,113 @@ +import wallpaper from "service/wallpaper" +import options from "options" +import { sh, dependencies } from "./utils" + +export default function init() { + wallpaper.connect("changed", () => matugen()) + options.autotheme.connect("changed", () => matugen()) +} + +function animate(...setters: Array<() => void>) { + const delay = options.transition.value / 2 + setters.forEach((fn, i) => Utils.timeout(delay * i, fn)) +} + +export async function matugen( + type: "image" | "color" = "image", + arg = wallpaper.wallpaper, +) { + if (!options.autotheme.value || !dependencies("matugen")) + return + + const colors = await sh(`matugen --dry-run -j hex ${type} ${arg}`) + const c = JSON.parse(colors).colors as { light: Colors, dark: Colors } + const { dark, light } = options.theme + + animate( + () => { + dark.widget.value = c.dark.on_surface + light.widget.value = c.light.on_surface + }, + () => { + dark.border.value = c.dark.outline + light.border.value = c.light.outline + }, + () => { + dark.bg.value = c.dark.surface + light.bg.value = c.light.surface + }, + () => { + dark.fg.value = c.dark.on_surface + light.fg.value = c.light.on_surface + }, + () => { + dark.primary.bg.value = c.dark.primary + light.primary.bg.value = c.light.primary + options.bar.battery.charging.value = options.theme.scheme.value === "dark" + ? c.dark.primary : c.light.primary + }, + () => { + dark.primary.fg.value = c.dark.on_primary + light.primary.fg.value = c.light.on_primary + }, + () => { + dark.error.bg.value = c.dark.error + light.error.bg.value = c.light.error + }, + () => { + dark.error.fg.value = c.dark.on_error + light.error.fg.value = c.light.on_error + }, + ) +} + +type Colors = { + background: string + error: string + error_container: string + inverse_on_surface: string + inverse_primary: string + inverse_surface: string + on_background: string + on_error: string + on_error_container: string + on_primary: string + on_primary_container: string + on_primary_fixed: string + on_primary_fixed_variant: string + on_secondary: string + on_secondary_container: string + on_secondary_fixed: string + on_secondary_fixed_variant: string + on_surface: string + on_surface_variant: string + on_tertiary: string + on_tertiary_container: string + on_tertiary_fixed: string + on_tertiary_fixed_variant: string + outline: string + outline_variant: string + primary: string + primary_container: string + primary_fixed: string + primary_fixed_dim: string + scrim: string + secondary: string + secondary_container: string + secondary_fixed: string + secondary_fixed_dim: string + shadow: string + surface: string + surface_bright: string + surface_container: string + surface_container_high: string + surface_container_highest: string + surface_container_low: string + surface_container_lowest: string + surface_dim: string + surface_variant: string + tertiary: string + tertiary_container: string + tertiary_fixed: string + tertiary_fixed_dim: string +} diff --git a/home/redyf/desktop/addons/ags/config/lib/notifications.ts b/home/redyf/desktop/addons/ags/config/lib/notifications.ts new file mode 100644 index 00000000..00008310 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/notifications.ts @@ -0,0 +1,16 @@ +import options from "options" +const notifs = await Service.import("notifications") + +// TODO: consider adding this to upstream + +const { blacklist } = options.notifications + +export default function init() { + const notify = notifs.constructor.prototype.Notify.bind(notifs) + notifs.constructor.prototype.Notify = function(appName: string, ...rest: unknown[]) { + if (blacklist.value.includes(appName)) + return Number.MAX_SAFE_INTEGER + + return notify(appName, ...rest) + } +} diff --git a/home/redyf/desktop/addons/ags/config/lib/option.ts b/home/redyf/desktop/addons/ags/config/lib/option.ts new file mode 100644 index 00000000..2d739783 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/option.ts @@ -0,0 +1,115 @@ +import { Variable } from "resource:///com/github/Aylur/ags/variable.js" + +type OptProps = { + persistent?: boolean +} + +export class Opt extends Variable { + static { Service.register(this) } + + constructor(initial: T, { persistent = false }: OptProps = {}) { + super(initial) + this.initial = initial + this.persistent = persistent + } + + initial: T + id = "" + persistent: boolean + toString() { return `${this.value}` } + toJSON() { return `opt:${this.value}` } + + getValue = (): T => { + return super.getValue() + } + + init(cacheFile: string) { + const cacheV = JSON.parse(Utils.readFile(cacheFile) || "{}")[this.id] + if (cacheV !== undefined) + this.value = cacheV + + this.connect("changed", () => { + const cache = JSON.parse(Utils.readFile(cacheFile) || "{}") + cache[this.id] = this.value + Utils.writeFileSync(JSON.stringify(cache, null, 2), cacheFile) + }) + } + + reset() { + if (this.persistent) + return + + if (JSON.stringify(this.value) !== JSON.stringify(this.initial)) { + this.value = this.initial + return this.id + } + } +} + +export const opt = (initial: T, opts?: OptProps) => new Opt(initial, opts) + +function getOptions(object: object, path = ""): Opt[] { + return Object.keys(object).flatMap(key => { + const obj: Opt = object[key] + const id = path ? path + "." + key : key + + if (obj instanceof Variable) { + obj.id = id + return obj + } + + if (typeof obj === "object") + return getOptions(obj, id) + + return [] + }) +} + +export function mkOptions(cacheFile: string, object: T) { + for (const opt of getOptions(object)) + opt.init(cacheFile) + + Utils.ensureDirectory(cacheFile.split("/").slice(0, -1).join("/")) + + const configFile = `${TMP}/config.json` + const values = getOptions(object).reduce((obj, { id, value }) => ({ [id]: value, ...obj }), {}) + Utils.writeFileSync(JSON.stringify(values, null, 2), configFile) + Utils.monitorFile(configFile, () => { + const cache = JSON.parse(Utils.readFile(configFile) || "{}") + for (const opt of getOptions(object)) { + if (JSON.stringify(cache[opt.id]) !== JSON.stringify(opt.value)) + opt.value = cache[opt.id] + } + }) + + function sleep(ms = 0) { + return new Promise(r => setTimeout(r, ms)) + } + + async function reset( + [opt, ...list] = getOptions(object), + id = opt?.reset(), + ): Promise> { + if (!opt) + return sleep().then(() => []) + + return id + ? [id, ...(await sleep(50).then(() => reset(list)))] + : await sleep().then(() => reset(list)) + } + + return Object.assign(object, { + configFile, + array: () => getOptions(object), + async reset() { + return (await reset()).join("\n") + }, + handler(deps: string[], callback: () => void) { + for (const opt of getOptions(object)) { + if (deps.some(i => opt.id.startsWith(i))) + opt.connect("changed", callback) + } + }, + }) +} + diff --git a/home/redyf/desktop/addons/ags/config/lib/session.ts b/home/redyf/desktop/addons/ags/config/lib/session.ts new file mode 100644 index 00000000..0e3e0cf8 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/session.ts @@ -0,0 +1,16 @@ +import GLib from "gi://GLib?version=2.0" + +declare global { + const OPTIONS: string + const TMP: string + const USER: string +} + +Object.assign(globalThis, { + OPTIONS: `${GLib.get_user_cache_dir()}/ags/options.json`, + TMP: `${GLib.get_tmp_dir()}/asztal`, + USER: GLib.get_user_name(), +}) + +Utils.ensureDirectory(TMP) +App.addIcons(`${App.configDir}/assets`) diff --git a/home/redyf/desktop/addons/ags/config/lib/tmux.ts b/home/redyf/desktop/addons/ags/config/lib/tmux.ts new file mode 100644 index 00000000..210bd219 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/tmux.ts @@ -0,0 +1,14 @@ +import options from "options" +import { sh } from "./utils" + +export async function tmux() { + const { scheme, dark, light } = options.theme + const hex = scheme.value === "dark" ? dark.primary.bg.value : light.primary.bg.value + if (await sh("which tmux").catch(() => false)) + sh(`tmux set @main_accent "${hex}"`) +} + +export default function init() { + options.theme.dark.primary.bg.connect("changed", tmux) + options.theme.light.primary.bg.connect("changed", tmux) +} diff --git a/home/redyf/desktop/addons/ags/config/lib/utils.ts b/home/redyf/desktop/addons/ags/config/lib/utils.ts new file mode 100644 index 00000000..425f4557 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/utils.ts @@ -0,0 +1,113 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { type Application } from "types/service/applications" +import icons, { substitutes } from "./icons" +import Gtk from "gi://Gtk?version=3.0" +import Gdk from "gi://Gdk" +import GLib from "gi://GLib?version=2.0" + +export type Binding = import("types/service").Binding + +/** + * @returns substitute icon || name || fallback icon + */ +export function icon(name: string | null, fallback = icons.missing) { + if (!name) + return fallback || "" + + if (GLib.file_test(name, GLib.FileTest.EXISTS)) + return name + + const icon = (substitutes[name] || name) + if (Utils.lookUpIcon(icon)) + return icon + + print(`no icon substitute "${icon}" for "${name}", fallback: "${fallback}"`) + return fallback +} + +/** + * @returns execAsync(["bash", "-c", cmd]) + */ +export async function bash(strings: TemplateStringsArray | string, ...values: unknown[]) { + const cmd = typeof strings === "string" ? strings : strings + .flatMap((str, i) => str + `${values[i] ?? ""}`) + .join("") + + return Utils.execAsync(["bash", "-c", cmd]).catch(err => { + console.error(cmd, err) + return "" + }) +} + +/** + * @returns execAsync(cmd) + */ +export async function sh(cmd: string | string[]) { + return Utils.execAsync(cmd).catch(err => { + console.error(typeof cmd === "string" ? cmd : cmd.join(" "), err) + return "" + }) +} + +export function forMonitors(widget: (monitor: number) => Gtk.Window) { + const n = Gdk.Display.get_default()?.get_n_monitors() || 1 + return range(n, 0).flatMap(widget) +} + +/** + * @returns [start...length] + */ +export function range(length: number, start = 1) { + return Array.from({ length }, (_, i) => i + start) +} + +/** + * @returns true if all of the `bins` are found + */ +export function dependencies(...bins: string[]) { + const missing = bins.filter(bin => Utils.exec({ + cmd: `which ${bin}`, + out: () => false, + err: () => true, + })) + + if (missing.length > 0) { + console.warn(Error(`missing dependencies: ${missing.join(", ")}`)) + Utils.notify(`missing dependencies: ${missing.join(", ")}`) + } + + return missing.length === 0 +} + +/** + * run app detached + */ +export function launchApp(app: Application) { + const exe = app.executable + .split(/\s+/) + .filter(str => !str.startsWith("%") && !str.startsWith("@")) + .join(" ") + + bash(`${exe} &`) + app.frequency += 1 +} + +/** + * to use with drag and drop + */ +export function createSurfaceFromWidget(widget: Gtk.Widget) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const cairo = imports.gi.cairo as any + const alloc = widget.get_allocation() + const surface = new cairo.ImageSurface( + cairo.Format.ARGB32, + alloc.width, + alloc.height, + ) + const cr = new cairo.Context(surface) + cr.setSourceRGBA(255, 255, 255, 0) + cr.rectangle(0, 0, alloc.width, alloc.height) + cr.fill() + widget.draw(cr) + return surface +} diff --git a/home/redyf/desktop/addons/ags/config/lib/variables.ts b/home/redyf/desktop/addons/ags/config/lib/variables.ts new file mode 100644 index 00000000..19308cd7 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/lib/variables.ts @@ -0,0 +1,42 @@ +import GLib from "gi://GLib" +// import options from "options" +// +// const intval = options.system.fetchInterval.value +// const tempPath = options.system.temperature.value + +export const clock = Variable(GLib.DateTime.new_now_local(), { + poll: [1000, () => GLib.DateTime.new_now_local()], +}) + +export const uptime = Variable(0, { + poll: [60_000, "cat /proc/uptime", line => + Number.parseInt(line.split(".")[0]) / 60, + ], +}) + +export const distro = { + id: GLib.get_os_info("ID"), + logo: GLib.get_os_info("LOGO"), +} + +// const divide = ([total, free]: string[]) => Number.parseInt(free) / Number.parseInt(total) +// +// export const cpu = Variable(0, { +// poll: [intval, "top -b -n 1", out => divide(["100", out.split("\n") +// .find(line => line.includes("Cpu(s)")) +// ?.split(/\s+/)[1] +// .replace(",", ".") || "0"])], +// }) +// +// export const ram = Variable(0, { +// poll: [intval, "free", out => divide(out.split("\n") +// .find(line => line.includes("Mem:")) +// ?.split(/\s+/) +// .splice(1, 2) || ["1", "1"])], +// }) +// +// export const temperature = Variable(0, { +// poll: [intval, `cat ${tempPath}`, n => { +// return Number.parseInt(n) / 100_000 +// }], +// }) diff --git a/home/redyf/desktop/addons/ags/config/main.ts b/home/redyf/desktop/addons/ags/config/main.ts new file mode 100644 index 00000000..f0459828 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/main.ts @@ -0,0 +1,41 @@ +import "lib/session" +import "style/style" +import init from "lib/init" +import options from "options" +import Bar from "widget/bar/Bar" +import Launcher from "widget/launcher/Launcher" +import NotificationPopups from "widget/notifications/NotificationPopups" +import OSD from "widget/osd/OSD" +import Overview from "widget/overview/Overview" +import PowerMenu from "widget/powermenu/PowerMenu" +import ScreenCorners from "widget/bar/ScreenCorners" +import SettingsDialog from "widget/settings/SettingsDialog" +import Verification from "widget/powermenu/Verification" +import { forMonitors } from "lib/utils" +import { setupQuickSettings } from "widget/quicksettings/QuickSettings" +import { setupDateMenu } from "widget/datemenu/DateMenu" + +App.config({ + onConfigParsed: () => { + setupQuickSettings() + setupDateMenu() + init() + }, + closeWindowDelay: { + "launcher": options.transition.value, + "overview": options.transition.value, + "quicksettings": options.transition.value, + "datemenu": options.transition.value, + }, + windows: () => [ + ...forMonitors(Bar), + ...forMonitors(NotificationPopups), + //...forMonitors(ScreenCorners), + ...forMonitors(OSD), + Launcher(), + Overview(), + PowerMenu(), + SettingsDialog(), + Verification(), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/options.ts b/home/redyf/desktop/addons/ags/config/options.ts new file mode 100644 index 00000000..c59fe1d5 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/options.ts @@ -0,0 +1,243 @@ +import { opt, mkOptions } from "lib/option" +import { distro } from "lib/variables" +import { icon } from "lib/utils" +import icons from "lib/icons" + +const options = mkOptions(OPTIONS, { + autotheme: opt(false), + + wallpaper: { + resolution: opt(1920), + market: opt("random"), + }, + + theme: { + dark: { + primary: { + bg: opt("#51a4e7"), + fg: opt("#141414"), + }, + error: { + bg: opt("#e55f86"), + fg: opt("#141414"), + }, + bg: opt("#171717"), + fg: opt("#eeeeee"), + widget: opt("#eeeeee"), + border: opt("#eeeeee"), + }, + light: { + primary: { + bg: opt("#426ede"), + fg: opt("#eeeeee"), + }, + error: { + bg: opt("#b13558"), + fg: opt("#eeeeee"), + }, + bg: opt("#fffffa"), + fg: opt("#080808"), + widget: opt("#080808"), + border: opt("#080808"), + }, + + blur: opt(0), + scheme: opt<"dark" | "light">("dark"), + widget: { opacity: opt(94) }, + border: { + width: opt(1), + opacity: opt(96), + }, + + shadows: opt(true), + padding: opt(7), + spacing: opt(12), + radius: opt(11), + }, + + transition: opt(200), + + font: { + size: opt(13), + name: opt("Ubuntu Nerd Font"), + }, + + bar: { + flatButtons: opt(true), + position: opt<"top" | "bottom">("top"), + corners: opt(true), + transparent: opt(false), + layout: { + start: opt>([ + "launcher", + "workspaces", + "taskbar", + "expander", + "messages", + ]), + center: opt>([ + "date", + ]), + end: opt>([ + "media", + "expander", + "systray", + "colorpicker", + "screenrecord", + "system", + "battery", + "powermenu", + ]), + }, + launcher: { + icon: { + colored: opt(true), + icon: opt(icon(distro.logo, icons.ui.search)), + }, + label: { + colored: opt(false), + label: opt(" Applications"), + }, + action: opt(() => App.toggleWindow("launcher")), + }, + date: { + format: opt("%H:%M - %A %e."), + action: opt(() => App.toggleWindow("datemenu")), + }, + battery: { + bar: opt<"hidden" | "regular" | "whole">("regular"), + charging: opt("#00D787"), + percentage: opt(true), + blocks: opt(7), + width: opt(50), + low: opt(30), + }, + workspaces: { + workspaces: opt(7), + }, + taskbar: { + iconSize: opt(0), + monochrome: opt(true), + exclusive: opt(false), + }, + messages: { + action: opt(() => App.toggleWindow("datemenu")), + }, + systray: { + ignore: opt([ + "KDE Connect Indicator", + "spotify-client", + ]), + }, + media: { + monochrome: opt(true), + preferred: opt("spotify"), + direction: opt<"left" | "right">("right"), + format: opt("{artists} - {title}"), + length: opt(40), + }, + powermenu: { + monochrome: opt(false), + action: opt(() => App.toggleWindow("powermenu")), + }, + }, + + launcher: { + width: opt(0), + margin: opt(80), + nix: { + pkgs: opt("nixpkgs/nixos-unstable"), + max: opt(8), + }, + sh: { + max: opt(16), + }, + apps: { + iconSize: opt(62), + max: opt(6), + favorites: opt([ + [ + "firefox", + "wezterm", + "org.gnome.Nautilus", + "org.gnome.Calendar", + "spotify", + ], + ]), + }, + }, + + overview: { + scale: opt(9), + workspaces: opt(7), + monochromeIcon: opt(true), + }, + + powermenu: { + sleep: opt("systemctl suspend"), + reboot: opt("systemctl reboot"), + logout: opt("pkill Hyprland"), + shutdown: opt("shutdown now"), + layout: opt<"line" | "box">("line"), + labels: opt(true), + }, + + quicksettings: { + avatar: { + image: opt(`/var/lib/AccountsService/icons/${Utils.USER}`), + size: opt(70), + }, + width: opt(380), + position: opt<"left" | "center" | "right">("right"), + networkSettings: opt("gtk-launch gnome-control-center"), + media: { + monochromeIcon: opt(true), + coverSize: opt(100), + }, + }, + + datemenu: { + position: opt<"left" | "center" | "right">("center"), + weather: { + interval: opt(60_000), + unit: opt<"metric" | "imperial" | "standard">("metric"), + key: opt( + JSON.parse(Utils.readFile(`${App.configDir}/.weather`) || "{}")?.key || "", + ), + cities: opt>( + JSON.parse(Utils.readFile(`${App.configDir}/.weather`) || "{}")?.cities || [], + ), + }, + }, + + osd: { + progress: { + vertical: opt(true), + pack: { + h: opt<"start" | "center" | "end">("end"), + v: opt<"start" | "center" | "end">("center"), + }, + }, + microphone: { + pack: { + h: opt<"start" | "center" | "end">("center"), + v: opt<"start" | "center" | "end">("end"), + }, + }, + }, + + notifications: { + position: opt>(["top", "right"]), + blacklist: opt(["Spotify"]), + width: opt(440), + }, + + hyprland: { + gaps: opt(2.4), + inactiveBorder: opt("#282828"), + gapsWhenOnly: opt(true), + }, +}) + +globalThis["options"] = options +export default options diff --git a/home/redyf/desktop/addons/ags/config/package.json b/home/redyf/desktop/addons/ags/config/package.json new file mode 100644 index 00000000..bc86b1be --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/package.json @@ -0,0 +1,19 @@ +{ + "name": "ags-dotfiles", + "author": "Aylur", + "kofi": "https://ko-fi.com/aylur", + "repository": { + "type": "git", + "url": "git+https://github.com/Aylur/dotfiles.git" + }, + "devDependencies": { + "@girs/accountsservice-1.0": "^1.0.0-3.2.7", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "eslint": "^8.56.0", + "eslint-config-standard-with-typescript": "^43.0.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-promise": "^6.1.1", + "typescript": "^5.3.3" + } +} diff --git a/home/redyf/desktop/addons/ags/config/service/asusctl.ts b/home/redyf/desktop/addons/ags/config/service/asusctl.ts new file mode 100644 index 00000000..6c2d071b --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/asusctl.ts @@ -0,0 +1,55 @@ +import { sh } from "lib/utils" + +type Profile = "Performance" | "Balanced" | "Quiet" +type Mode = "Hybrid" | "Integrated" + +class Asusctl extends Service { + static { + Service.register(this, {}, { + "profile": ["string", "r"], + "mode": ["string", "r"], + }) + } + + get available() { + return Utils.exec("which asusctl", () => true, () => false) + } + + #profile: Profile = "Balanced" + #mode: Mode = "Hybrid" + + async nextProfile() { + await sh("asusctl profile -n") + const profile = await sh("asusctl profile -p") + const p = profile.split(" ")[3] as Profile + this.#profile = p + this.changed("profile") + } + + async setProfile(prof: Profile) { + await sh(`asusctl profile --profile-set ${prof}`) + this.#profile = prof + this.changed("profile") + } + + async nextMode() { + await sh(`supergfxctl -m ${this.#mode === "Hybrid" ? "Integrated" : "Hybrid"}`) + this.#mode = await sh("supergfxctl -g") as Mode + this.changed("profile") + } + + constructor() { + super() + + if (this.available) { + sh("asusctl profile -p").then(p => this.#profile = p.split(" ")[3] as Profile) + sh("supergfxctl -g").then(m => this.#mode = m as Mode) + } + } + + get profiles(): Profile[] { return ["Performance", "Balanced", "Quiet"] } + get profile() { return this.#profile } + get mode() { return this.#mode } +} + +export default new Asusctl diff --git a/home/redyf/desktop/addons/ags/config/service/brightness.ts b/home/redyf/desktop/addons/ags/config/service/brightness.ts new file mode 100644 index 00000000..f229ebcc --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/brightness.ts @@ -0,0 +1,69 @@ +import { bash, dependencies, sh } from "lib/utils" + +if (!dependencies("brightnessctl")) + App.quit() + +const get = (args: string) => Number(Utils.exec(`brightnessctl ${args}`)) +const screen = await bash`ls -w1 /sys/class/backlight | head -1` +const kbd = await bash`ls -w1 /sys/class/leds | head -1` + +class Brightness extends Service { + static { + Service.register(this, {}, { + "screen": ["float", "rw"], + "kbd": ["int", "rw"], + }) + } + + #kbdMax = get(`--device ${kbd} max`) + #kbd = get(`--device ${kbd} get`) + #screenMax = get("max") + #screen = get("get") / (get("max") || 1) + + get kbd() { return this.#kbd } + get screen() { return this.#screen } + + set kbd(value) { + if (value < 0 || value > this.#kbdMax) + return + + sh(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { + this.#kbd = value + this.changed("kbd") + }) + } + + set screen(percent) { + if (percent < 0) + percent = 0 + + if (percent > 1) + percent = 1 + + sh(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => { + this.#screen = percent + this.changed("screen") + }) + } + + constructor() { + super() + + const screenPath = `/sys/class/backlight/${screen}/brightness` + const kbdPath = `/sys/class/leds/${kbd}/brightness` + + Utils.monitorFile(screenPath, async f => { + const v = await Utils.readFileAsync(f) + this.#screen = Number(v) / this.#screenMax + this.changed("screen") + }) + + Utils.monitorFile(kbdPath, async f => { + const v = await Utils.readFileAsync(f) + this.#kbd = Number(v) / this.#kbdMax + this.changed("kbd") + }) + } +} + +export default new Brightness diff --git a/home/redyf/desktop/addons/ags/config/service/colorpicker.ts b/home/redyf/desktop/addons/ags/config/service/colorpicker.ts new file mode 100644 index 00000000..5918f31c --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/colorpicker.ts @@ -0,0 +1,56 @@ +import icons from "lib/icons" +import { bash, dependencies } from "lib/utils" + +const COLORS_CACHE = Utils.CACHE_DIR + "/colorpicker.json" +const MAX_NUM_COLORS = 10 + +class ColorPicker extends Service { + static { + Service.register(this, {}, { + "colors": ["jsobject"], + }) + } + + #notifID = 0 + #colors = JSON.parse(Utils.readFile(COLORS_CACHE) || "[]") as string[] + + get colors() { return [...this.#colors] } + set colors(colors) { + this.#colors = colors + this.changed("colors") + } + + // TODO: doesn't work? + async wlCopy(color: string) { + if (dependencies("wl-copy")) + bash(`wl-copy ${color}`) + } + + readonly pick = async () => { + if (!dependencies("hyprpicker")) + return + + const color = await bash("hyprpicker -a -r") + if (!color) + return + + this.wlCopy(color) + const list = this.colors + if (!list.includes(color)) { + list.push(color) + if (list.length > MAX_NUM_COLORS) + list.shift() + + this.colors = list + Utils.writeFile(JSON.stringify(list, null, 2), COLORS_CACHE) + } + + this.#notifID = await Utils.notify({ + id: this.#notifID, + iconName: icons.ui.colorpicker, + summary: color, + }) + } +} + +export default new ColorPicker diff --git a/home/redyf/desktop/addons/ags/config/service/nix.ts b/home/redyf/desktop/addons/ags/config/service/nix.ts new file mode 100644 index 00000000..920f5721 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/nix.ts @@ -0,0 +1,111 @@ +import icons from "lib/icons" +import { bash, dependencies } from "lib/utils" +import options from "options" + +const CACHE = `${Utils.CACHE_DIR}/nixpkgs` +const PREFIX = "legacyPackages.x86_64-linux." +const MAX = options.launcher.nix.max +const nixpkgs = options.launcher.nix.pkgs + +export type Nixpkg = { + name: string + description: string + pname: string + version: string +} + +class Nix extends Service { + static { + Service.register(this, {}, { + "available": ["boolean", "r"], + "ready": ["boolean", "rw"], + }) + } + + #db: { [name: string]: Nixpkg } = {} + #ready = true + + private set ready(r: boolean) { + this.#ready = r + this.changed("ready") + } + + get db() { return this.#db } + get ready() { return this.#ready } + get available() { + return Utils.exec("which nix", () => true, () => false) + } + + constructor() { + super() + if (!this.available) + return this + + this.#updateList() + nixpkgs.connect("changed", this.#updateList) + } + + query = async (filter: string) => { + if (!dependencies("fzf", "nix") || !this.#ready) + return [] as string[] + + return bash(`cat ${CACHE} | fzf -f ${filter} -e | head -n ${MAX} `) + .then(str => str.split("\n").filter(i => i)) + } + + nix(cmd: string, bin: string, args: string) { + return Utils.execAsync(`nix ${cmd} ${nixpkgs}#${bin} --impure ${args}`) + } + + run = async (input: string) => { + if (!dependencies("nix")) + return + + try { + const [bin, ...args] = input.trim().split(/\s+/) + + this.ready = false + await this.nix("shell", bin, "--command sh -c 'exit'") + this.ready = true + + this.nix("run", bin, ["--", ...args].join(" ")) + } catch (err) { + if (typeof err === "string") + Utils.notify("NixRun Error", err, icons.nix.nix) + else + logError(err) + } finally { + this.ready = true + } + } + + #updateList = async () => { + if (!dependencies("nix")) + return + + this.ready = false + this.#db = {} + + // const search = await bash(`nix search ${nixpkgs} --json`) + const search = "" + if (!search) { + this.ready = true + return + } + + const json = Object.entries(JSON.parse(search) as { + [name: string]: Nixpkg + }) + + for (const [pkg, info] of json) { + const name = pkg.replace(PREFIX, "") + this.#db[name] = { ...info, name } + } + + const list = Object.keys(this.#db).join("\n") + await Utils.writeFile(list, CACHE) + this.ready = true + } +} + +export default new Nix diff --git a/home/redyf/desktop/addons/ags/config/service/powermenu.ts b/home/redyf/desktop/addons/ags/config/service/powermenu.ts new file mode 100644 index 00000000..039f8b04 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/powermenu.ts @@ -0,0 +1,47 @@ +import options from "options" + +const { sleep, reboot, logout, shutdown } = options.powermenu + +export type Action = "sleep" | "reboot" | "logout" | "shutdown" + +class PowerMenu extends Service { + static { + Service.register(this, {}, { + "title": ["string"], + "cmd": ["string"], + }) + } + + #title = "" + #cmd = "" + + get title() { return this.#title } + + action(action: Action) { + [this.#cmd, this.#title] = { + sleep: [sleep.value, "Sleep"], + reboot: [reboot.value, "Reboot"], + logout: [logout.value, "Log Out"], + shutdown: [shutdown.value, "Shutdown"], + }[action] + + this.notify("cmd") + this.notify("title") + this.emit("changed") + App.closeWindow("powermenu") + App.openWindow("verification") + } + + readonly shutdown = () => { + this.action("shutdown") + } + + readonly exec = () => { + App.closeWindow("verification") + Utils.exec(this.#cmd) + } +} + +const powermenu = new PowerMenu +Object.assign(globalThis, { powermenu }) +export default powermenu diff --git a/home/redyf/desktop/addons/ags/config/service/screenrecord.ts b/home/redyf/desktop/addons/ags/config/service/screenrecord.ts new file mode 100644 index 00000000..58721d2d --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/screenrecord.ts @@ -0,0 +1,102 @@ +import GLib from "gi://GLib" +import icons from "lib/icons" +import { dependencies, sh, bash } from "lib/utils" + +const now = () => GLib.DateTime.new_now_local().format("%Y-%m-%d_%H-%M-%S") + +class Recorder extends Service { + static { + Service.register(this, {}, { + "timer": ["int"], + "recording": ["boolean"], + }) + } + + #recordings = Utils.HOME + "/Videos/Screencasting" + #screenshots = Utils.HOME + "/Pictures/Screenshots" + #file = "" + #interval = 0 + + recording = false + timer = 0 + + async start() { + if (!dependencies("slurp", "wf-recorder")) + return + + if (this.recording) + return + + Utils.ensureDirectory(this.#recordings) + this.#file = `${this.#recordings}/${now()}.mp4` + sh(`wf-recorder -g "${await sh("slurp")}" -f ${this.#file} --pixel-format yuv420p`) + + this.recording = true + this.changed("recording") + + this.timer = 0 + this.#interval = Utils.interval(1000, () => { + this.changed("timer") + this.timer++ + }) + } + + async stop() { + if (!this.recording) + return + + await bash("killall -INT wf-recorder") + this.recording = false + this.changed("recording") + GLib.source_remove(this.#interval) + + Utils.notify({ + iconName: icons.fallback.video, + summary: "Screenrecord", + body: this.#file, + actions: { + "Show in Files": () => sh(`xdg-open ${this.#recordings}`), + "View": () => sh(`xdg-open ${this.#file}`), + }, + }) + } + + async screenshot(full = false) { + if (!dependencies("slurp", "wayshot")) + return + + const file = `${this.#screenshots}/${now()}.png` + Utils.ensureDirectory(this.#screenshots) + + if (full) { + await sh(`wayshot -f ${file}`) + } + else { + const size = await sh("slurp") + if (!size) + return + + await sh(`wayshot -f ${file} -s "${size}"`) + } + + bash(`wl-copy < ${file}`) + + Utils.notify({ + image: file, + summary: "Screenshot", + body: file, + actions: { + "Show in Files": () => sh(`xdg-open ${this.#screenshots}`), + "View": () => sh(`xdg-open ${file}`), + "Edit": () => { + if (dependencies("swappy")) + sh(`swappy -f ${file}`) + }, + }, + }) + } +} + +const recorder = new Recorder +Object.assign(globalThis, { recorder }) +export default recorder diff --git a/home/redyf/desktop/addons/ags/config/service/sh.ts b/home/redyf/desktop/addons/ags/config/service/sh.ts new file mode 100644 index 00000000..eb5f20bf --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/sh.ts @@ -0,0 +1,48 @@ +import GLib from "gi://GLib?version=2.0" +import { bash, dependencies } from "lib/utils" +import icons from "lib/icons" +import options from "options" + +const MAX = options.launcher.sh.max +const BINS = `${Utils.CACHE_DIR}/binaries` + +async function ls(path: string) { + return Utils.execAsync(`ls ${path}`).catch(() => "") +} + +async function reload() { + const bins = await Promise.all(GLib.getenv("PATH")! + .split(":") + .map(ls)) + + Utils.writeFile(bins.join("\n"), BINS) +} + +async function query(filter: string) { + if (!dependencies("fzf")) + return [] as string[] + + return bash(`cat ${BINS} | fzf -f ${filter} | head -n ${MAX}`) + .then(str => Array.from(new Set(str.split("\n").filter(i => i)).values())) + .catch(err => { print(err); return [] }) +} + +function run(args: string) { + Utils.execAsync(args) + .then(out => { + print(`:sh ${args.trim()}:`) + print(out) + }) + .catch(err => { + Utils.notify("ShRun Error", err, icons.app.terminal) + }) +} + +class Sh extends Service { + static { Service.register(this) } + constructor() { super(); reload() } + query = query + run = run +} + +export default new Sh diff --git a/home/redyf/desktop/addons/ags/config/service/wallpaper.ts b/home/redyf/desktop/addons/ags/config/service/wallpaper.ts new file mode 100644 index 00000000..47ff8d43 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/wallpaper.ts @@ -0,0 +1,99 @@ +import options from "options" +import { dependencies, sh } from "lib/utils" + +export type Resolution = 1920 | 1366 | 3840 +export type Market = + | "random" + | "en-US" + | "ja-JP" + | "en-AU" + | "en-GB" + | "de-DE" + | "en-NZ" + | "en-CA" + +const WP = `${Utils.HOME}/.config/background` +const Cache = `${Utils.HOME}/Pictures/Wallpapers/Bing` + +class Wallpaper extends Service { + static { + Service.register(this, {}, { + "wallpaper": ["string"], + }) + } + + #blockMonitor = false + + #wallpaper() { + if (!dependencies("swww")) + return + + sh("hyprctl cursorpos").then(pos => { + sh([ + "swww", "img", + "--invert-y", + "--transition-type", "grow", + "--transition-pos", pos.replace(" ", ""), + WP, + ]).then(() => { + this.changed("wallpaper") + }) + }) + } + + async #setWallpaper(path: string) { + this.#blockMonitor = true + + await sh(`cp ${path} ${WP}`) + this.#wallpaper() + + this.#blockMonitor = false + } + + async #fetchBing() { + const res = await Utils.fetch("https://bing.biturl.top/", { + params: { + resolution: options.wallpaper.resolution.value, + format: "json", + image_format: "jpg", + index: "random", + mkt: options.wallpaper.market.value, + }, + }).then(res => res.text()) + + if (!res.startsWith("{")) + return console.warn("bing api", res) + + const { url } = JSON.parse(res) + const file = `${Cache}/${url.replace("https://www.bing.com/th?id=", "")}` + + if (dependencies("curl")) { + Utils.ensureDirectory(Cache) + await sh(`curl "${url}" --output ${file}`) + this.#setWallpaper(file) + } + } + + readonly random = () => { this.#fetchBing() } + readonly set = (path: string) => { this.#setWallpaper(path) } + get wallpaper() { return WP } + + constructor() { + super() + + if (!dependencies("swww")) + return this + + // gtk portal + Utils.monitorFile(WP, () => { + if (!this.#blockMonitor) + this.#wallpaper() + }) + + Utils.execAsync("swww-daemon") + .then(this.#wallpaper) + .catch(() => null) + } +} + +export default new Wallpaper diff --git a/home/redyf/desktop/addons/ags/config/service/weather.ts b/home/redyf/desktop/addons/ags/config/service/weather.ts new file mode 100644 index 00000000..14f2df26 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/service/weather.ts @@ -0,0 +1,59 @@ +import options from "options" + +const { interval, key, cities, unit } = options.datemenu.weather + +class Weather extends Service { + static { + Service.register(this, {}, { + "forecasts": ["jsobject"], + }) + } + + #forecasts: Forecast[] = [] + get forecasts() { return this.#forecasts } + + async #fetch(placeid: number) { + const url = "https://api.openweathermap.org/data/2.5/forecast" + const res = await Utils.fetch(url, { + params: { + id: placeid, + appid: key.value, + untis: unit.value, + }, + }) + return await res.json() + } + + constructor() { + super() + if (!key.value) + return this + + Utils.interval(interval.value, () => { + Promise.all(cities.value.map(this.#fetch)).then(forecasts => { + this.#forecasts = forecasts as Forecast[] + this.changed("forecasts") + }) + }) + } +} + +export default new Weather + +type Forecast = { + city: { + name: string, + } + list: Array<{ + dt: number + main: { + temp: number + feels_like: number + }, + weather: Array<{ + main: string, + description: string, + icon: string, + }> + }> +} diff --git a/home/redyf/desktop/addons/ags/config/style/extra.scss b/home/redyf/desktop/addons/ags/config/style/extra.scss new file mode 100644 index 00000000..e7f9d447 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/extra.scss @@ -0,0 +1,67 @@ +@import './mixins/button.scss'; + +* { + font-size: $font-size; + font-family: $font-name; +} + +separator { + &.horizontal { + min-height: $border-width; + } + + &.vertical { + min-width: $border-width; + } +} + +window.popup { + >* { + border: none; + box-shadow: none; + } + + menu { + border-radius: $popover-radius; + background-color: $bg; + padding: $popover-padding; + border: $border-width solid $popover-border-color; + + separator { + background-color: $border-color; + } + + menuitem { + @include button; + padding: $spacing * .5; + margin: ($spacing * .5) 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + } +} + +tooltip { + * { + all: unset; + } + + background-color: transparent; + border: none; + + >*>* { + background-color: $bg; + border-radius: $radius; + border: $border-width solid $popover-border-color; + color: $fg; + padding: 8px; + margin: 4px; + box-shadow: 0 0 3px 0 $shadow-color; + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/a11y-button.scss b/home/redyf/desktop/addons/ags/config/style/mixins/a11y-button.scss new file mode 100644 index 00000000..00b24c61 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/a11y-button.scss @@ -0,0 +1,48 @@ +@import './button'; + +@mixin accs-button($flat: false, $reactive: true) { + @include unset; + color: $fg; + + >* { + border-radius: $radius; + transition: $transition; + + @if $flat { + background-color: transparent; + box-shadow: none; + } + + @else { + background-color: $widget-bg; + box-shadow: inset 0 0 0 $border-width $border-color; + } + } + + + @if $reactive { + + &:focus>*, + &.focused>* { + @include button-focus; + } + + &:hover>* { + @include button-hover; + } + + &:active, + &.active, + &.on, + &:checked { + >* { + @include button-active; + } + + &:hover>* { + box-shadow: inset 0 0 0 $border-width $border-color, + inset 0 0 0 99px $hover-bg; + } + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/button.scss b/home/redyf/desktop/addons/ags/config/style/mixins/button.scss new file mode 100644 index 00000000..79ec2751 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/button.scss @@ -0,0 +1,70 @@ +@mixin button-focus() { + box-shadow: inset 0 0 0 $border-width $primary-bg; + background-color: $hover-bg; + color: $hover-fg; +} + +@mixin button-hover() { + box-shadow: inset 0 0 0 $border-width $border-color; + background-color: $hover-bg; + color: $hover-fg; +} + +@mixin button-active() { + box-shadow: inset 0 0 0 $border-width $border-color; + background-image: $active-gradient; + background-color: $primary-bg; + color: $primary-fg; +} + +@mixin button-disabled() { + box-shadow: none; + background-color: transparent; + color: transparentize($fg, 0.7); +} + +@mixin button($flat: false, $reactive: true, $radius: $radius, $focusable: true) { + all: unset; + transition: $transition; + border-radius: $radius; + color: $fg; + + @if $flat { + background-color: transparent; + background-image: none; + box-shadow: none; + } + + @else { + background-color: $widget-bg; + box-shadow: inset 0 0 0 $border-width $border-color; + } + + @if $reactive { + @if $focusable { + &:focus { + @include button-focus; + } + } + + &:hover { + @include button-hover; + } + + &:active, + &.on, + &.active, + &:checked { + @include button-active; + + &:hover { + box-shadow: inset 0 0 0 $border-width $border-color, + inset 0 0 0 99px $hover-bg; + } + } + } + + &:disabled { + @include button-disabled; + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/floating-widget.scss b/home/redyf/desktop/addons/ags/config/style/mixins/floating-widget.scss new file mode 100644 index 00000000..613668d7 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/floating-widget.scss @@ -0,0 +1,12 @@ +@mixin floating-widget { + @if $shadows { + box-shadow: 0 0 5px 0 $shadow-color; + } + + margin: max($spacing, 8px); + border: $border-width solid $popover-border-color; + background-color: $bg; + color: $fg; + border-radius: $popover-radius; + padding: $popover-padding; +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/hidden.scss b/home/redyf/desktop/addons/ags/config/style/mixins/hidden.scss new file mode 100644 index 00000000..ea6a42c2 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/hidden.scss @@ -0,0 +1,15 @@ +@mixin hidden { + background-color: transparent; + background-image: none; + border-color: transparent; + box-shadow: none; + -gtk-icon-transform: scale(0); + + * { + background-color: transparent; + background-image: none; + border-color: transparent; + box-shadow: none; + -gtk-icon-transform: scale(0); + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/media.scss b/home/redyf/desktop/addons/ags/config/style/mixins/media.scss new file mode 100644 index 00000000..31780297 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/media.scss @@ -0,0 +1,42 @@ +@mixin media() { + @include widget; + padding: $padding; + + .cover { + @if $shadows { + box-shadow: 2px 2px 2px 0 $shadow-color; + } + + background-size: cover; + background-position: center; + border-radius: $radius*0.8; + margin-right: $spacing; + } + + button { + @include button($flat: true); + padding: $padding * .5; + + &.play-pause { + margin: 0 ($spacing * .5); + } + + image { + font-size: 1.2em; + } + } + + .artist { + color: transparentize($fg, .2); + font-size: .9em; + } + + scale { + @include slider($width: .5em, $slider: false, $gradient: linear-gradient($fg, $fg)); + margin-bottom: $padding * .5; + + trough { + border: none; + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/scrollable.scss b/home/redyf/desktop/addons/ags/config/style/mixins/scrollable.scss new file mode 100644 index 00000000..b66f246a --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/scrollable.scss @@ -0,0 +1,42 @@ +@mixin scrollable($top: false, $bottom: false) { + + @if $top and $shadows { + undershoot.top { + background: linear-gradient(to bottom, $shadow-color, transparent, transparent, transparent, transparent, transparent); + } + } + + @if $bottom and $shadows { + undershoot.bottom { + background: linear-gradient(to top, $shadow-color, transparent, transparent, transparent, transparent, transparent); + } + } + + scrollbar, + scrollbar * { + all: unset; + } + + scrollbar.vertical { + transition: $transition; + background-color: transparentize($bg, 0.7); + + &:hover { + background-color: transparentize($bg, 0.3); + + slider { + background-color: transparentize($fg, 0.3); + min-width: .6em; + } + } + } + + + scrollbar.vertical slider { + background-color: transparentize($fg, 0.5); + border-radius: $radius; + min-width: .4em; + min-height: 2em; + transition: $transition; + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/slider.scss b/home/redyf/desktop/addons/ags/config/style/mixins/slider.scss new file mode 100644 index 00000000..b90e5669 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/slider.scss @@ -0,0 +1,74 @@ +@import './unset'; + +@mixin slider($width: 0.7em, $slider-width: .5em, $gradient: $active-gradient, $slider: true, $focusable: true, $radius: $radius) { + @include unset($rec: true); + + trough { + transition: $transition; + border-radius: $radius; + border: $border; + background-color: $widget-bg; + min-height: $width; + min-width: $width; + + highlight, + progress { + border-radius: max($radius - $border-width, 0); + background-image: $gradient; + min-height: $width; + min-width: $width; + } + } + + slider { + box-shadow: none; + background-color: transparent; + border: $border-width solid transparent; + transition: $transition; + border-radius: $radius; + min-height: $width; + min-width: $width; + margin: -$slider-width; + } + + &:hover { + trough { + background-color: $hover-bg; + } + + slider { + @if $slider { + background-color: $fg; + border-color: $border-color; + + @if $shadows { + box-shadow: 0 0 3px 0 $shadow-color; + } + } + } + } + + &:disabled { + + highlight, + progress { + background-color: transparentize($fg, 0.4); + background-image: none; + } + } + + @if $focusable { + trough:focus { + background-color: $hover-bg; + box-shadow: inset 0 0 0 $border-width $primary-bg; + + slider { + @if $slider { + background-color: $fg; + box-shadow: inset 0 0 0 $border-width $primary-bg; + } + } + } + + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/spacing.scss b/home/redyf/desktop/addons/ags/config/style/mixins/spacing.scss new file mode 100644 index 00000000..4096fbaa --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/spacing.scss @@ -0,0 +1,53 @@ +@mixin spacing($multiplier: 1, $spacing: $spacing, $rec: false) { + &.horizontal>* { + margin: 0 calc($spacing * $multiplier / 2); + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + + &.vertical>* { + margin: calc($spacing * $multiplier / 2) 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + + @if $rec { + box { + &.horizontal>* { + margin: 0 $spacing * $multiplier / 2; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + } + + &.vertical>* { + margin: $spacing * $multiplier / 2 0; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/switch.scss b/home/redyf/desktop/addons/ags/config/style/mixins/switch.scss new file mode 100644 index 00000000..2abf360a --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/switch.scss @@ -0,0 +1,16 @@ +@import './button'; + +@mixin switch { + @include button; + + slider { + background-color: $primary-fg; + border-radius: $radius; + min-width: 24px; + min-height: 24px; + } + + image { + color: transparent; + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/unset.scss b/home/redyf/desktop/addons/ags/config/style/mixins/unset.scss new file mode 100644 index 00000000..eb80af5c --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/unset.scss @@ -0,0 +1,9 @@ +@mixin unset($rec: false) { + all: unset; + + @if $rec { + * { + all: unset + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/mixins/widget.scss b/home/redyf/desktop/addons/ags/config/style/mixins/widget.scss new file mode 100644 index 00000000..053f1aa4 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/mixins/widget.scss @@ -0,0 +1,7 @@ +@mixin widget { + transition: $transition; + border-radius: $radius; + color: $fg; + background-color: $widget-bg; + border: $border; +} diff --git a/home/redyf/desktop/addons/ags/config/style/style.ts b/home/redyf/desktop/addons/ags/config/style/style.ts new file mode 100644 index 00000000..e9b86566 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/style.ts @@ -0,0 +1,110 @@ +/* eslint-disable max-len */ +import { type Opt } from "lib/option" +import options from "options" +import { bash, dependencies } from "lib/utils" + +const deps = [ + "font", + "theme", + "bar.flatButtons", + "bar.position", + "bar.battery.charging", + "bar.battery.blocks", +] + +const { + dark, + light, + blur, + scheme, + padding, + spacing, + radius, + shadows, + widget, + border, +} = options.theme + +const popoverPaddingMultiplier = 1.6 + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const t = (dark: Opt | string, light: Opt | string) => scheme.value === "dark" + ? `${dark}` : `${light}` + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const $ = (name: string, value: string | Opt) => `$${name}: ${value};` + +const variables = () => [ + $("bg", blur.value ? `transparentize(${t(dark.bg, light.bg)}, ${blur.value / 100})` : t(dark.bg, light.bg)), + $("fg", t(dark.fg, light.fg)), + + $("primary-bg", t(dark.primary.bg, light.primary.bg)), + $("primary-fg", t(dark.primary.fg, light.primary.fg)), + + $("error-bg", t(dark.error.bg, light.error.bg)), + $("error-fg", t(dark.error.fg, light.error.fg)), + + $("scheme", scheme), + $("padding", `${padding}pt`), + $("spacing", `${spacing}pt`), + $("radius", `${radius}px`), + $("transition", `${options.transition}ms`), + + $("shadows", `${shadows}`), + + $("widget-bg", `transparentize(${t(dark.widget, light.widget)}, ${widget.opacity.value / 100})`), + + $("hover-bg", `transparentize(${t(dark.widget, light.widget)}, ${(widget.opacity.value * .9) / 100})`), + $("hover-fg", `lighten(${t(dark.fg, light.fg)}, 8%)`), + + $("border-width", `${border.width}px`), + $("border-color", `transparentize(${t(dark.border, light.border)}, ${border.opacity.value / 100})`), + $("border", "$border-width solid $border-color"), + + $("active-gradient", `linear-gradient(to right, ${t(dark.primary.bg, light.primary.bg)}, darken(${t(dark.primary.bg, light.primary.bg)}, 4%))`), + $("shadow-color", t("rgba(0,0,0,.6)", "rgba(0,0,0,.4)")), + $("text-shadow", t("2pt 2pt 2pt $shadow-color", "none")), + $("box-shadow", t("2pt 2pt 2pt 0 $shadow-color, inset 0 0 0 $border-width $border-color", "none")), + + $("popover-border-color", `transparentize(${t(dark.border, light.border)}, ${Math.max(((border.opacity.value - 1) / 100), 0)})`), + $("popover-padding", `$padding * ${popoverPaddingMultiplier}`), + $("popover-radius", radius.value === 0 ? "0" : "$radius + $popover-padding"), + + $("font-size", `${options.font.size}pt`), + $("font-name", options.font.name), + + // etc + $("charging-bg", options.bar.battery.charging), + $("bar-battery-blocks", options.bar.battery.blocks), + $("bar-position", options.bar.position), + $("hyprland-gaps-multiplier", options.hyprland.gaps), +] + +async function resetCss() { + if (!dependencies("sass", "fd")) + return + + try { + const vars = `${TMP}/variables.scss` + const scss = `${TMP}/main.scss` + const css = `${TMP}/main.css` + + const fd = await bash(`fd ".scss" ${App.configDir}`) + const files = fd.split(/\s+/) + const imports = [vars, ...files].map(f => `@import '${f}';`) + + await Utils.writeFile(variables().join("\n"), vars) + await Utils.writeFile(imports.join("\n"), scss) + await bash`sass ${scss} ${css}` + + App.applyCss(css, true) + } catch (error) { + error instanceof Error + ? logError(error) + : console.error(error) + } +} + +Utils.monitorFile(`${App.configDir}/style`, resetCss) +options.handler(deps, resetCss) +await resetCss() diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/bar.scss b/home/redyf/desktop/addons/ags/config/style/widgets/bar.scss new file mode 100644 index 00000000..f3b0019b --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/bar.scss @@ -0,0 +1,265 @@ +@use 'sass:color'; + +$bar-spacing: $spacing * .3; +$button-radius: $radius; + +@mixin panel-button($flat: true, $reactive: true) { + @include accs-button($flat, $reactive); + + >* { + border-radius: $button-radius; + margin: $bar-spacing; + } + + label, + image { + font-weight: bold; + } + + >* { + padding: $padding * 0.4 $padding * 0.8; + } +} + +.bar { + transition: $transition; + background-color: $bg; + + .panel-button { + @include panel-button; + + &:not(.flat) { + + @include accs-button($flat: false); + } + } + + .launcher { + .colored { + color: transparentize($primary-bg, 0.2); + } + + &:hover .colored { + color: $primary-bg; + } + + &:active .colored, + &.active .colored { + color: $primary-fg; + } + } + + .workspaces { + label { + font-size: 0; + min-width: 5pt; + min-height: 5pt; + border-radius: $radius*.6; + box-shadow: inset 0 0 0 $border-width $border-color; + margin: 0 $padding * .5; + transition: $transition* .5; + background-color: transparentize($fg, .8); + + &.occupied { + background-color: transparentize($fg, .2); + min-width: 7pt; + min-height: 7pt; + } + + &.active { + // background-color: $primary-bg; + background-image: $active-gradient; + min-width: 20pt; + min-height: 12pt; + } + } + + &.active, + &:active { + label { + background-color: transparentize($primary-fg, .3); + + &.occupied { + background-color: transparentize($primary-fg, .15); + } + + &.active { + background-color: $primary-fg; + } + } + } + } + + .media label { + margin: 0 ($spacing * .5) + } + + .taskbar .indicator.active { + background-color: $primary-bg; + border-radius: $radius; + min-height: 4pt; + min-width: 6pt; + margin: 2pt; + } + + .powermenu.colored, + .recorder { + image { + color: transparentize($error-bg, 0.3); + } + + &:hover image { + color: transparentize($error-bg, 0.15); + } + + &:active image { + color: $primary-fg; + } + } + + .quicksettings>box>box { + @include spacing($spacing: if($bar-spacing==0, $padding / 2, $bar-spacing)); + } + + .quicksettings:not(.active):not(:active) { + .bluetooth { + color: $primary-bg; + + label { + font-size: $font-size * .7; + color: $fg; + text-shadow: $text-shadow; + } + } + } + + .battery-bar { + >* { + padding: 0; + } + + &.bar-hidden>box { + padding: 0 $spacing * .5; + + image { + margin: 0; + } + } + + levelbar * { + all: unset; + transition: $transition; + } + + .whole { + @if $shadows { + image { + -gtk-icon-shadow: $text-shadow; + } + + label { + text-shadow: $text-shadow; + } + } + } + + .regular image { + margin-left: $spacing * .5; + } + + trough { + @include widget; + min-height: 12pt; + min-width: 12pt; + } + + .regular trough { + margin-right: $spacing * .5; + } + + block { + margin: 0; + + &:last-child { + border-radius: 0 $button-radius $button-radius 0; + } + + &:first-child { + border-radius: $button-radius 0 0 $button-radius; + } + } + + .vertical { + block { + &:last-child { + border-radius: 0 0 $button-radius $button-radius; + } + + &:first-child { + border-radius: $button-radius $button-radius 0 0; + } + } + + } + + @for $i from 1 through $bar-battery-blocks { + block:nth-child(#{$i}).filled { + background-color: color.mix($bg, $primary-bg, $i*3) + } + + &.low block:nth-child(#{$i}).filled { + background-color: color.mix($bg, $error-bg, $i*3) + } + + &.charging block:nth-child(#{$i}).filled { + background-color: color.mix($bg, $charging-bg, $i*3) + } + + &:active .regular block:nth-child(#{$i}).filled { + background-color: color.mix($bg, $primary-fg, $i*3) + } + } + + &.low image { + color: $error-bg + } + + &.charging image { + color: $charging-bg + } + + &:active image { + color: $primary-fg + } + } +} + +.bar.transparent { + background-color: transparent; + + .panel-button { + &:hover>* { + box-shadow: 1px 1px 3px 0 $shadow-color, inset 0 0 0 $border-width $border-color; + background-color: $bg; + } + + &:not(:hover):not(.active) { + + label, + image { + text-shadow: $text-shadow; + -gtk-icon-shadow: $text-shadow; + } + } + } + + .workspaces label { + box-shadow: inset 0 0 0 $border-width $border-color, + 1px 1px 3px 0 $shadow-color; + } + + .battery-bar trough { + box-shadow: 1px 1px 3px 0 $shadow-color; + + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/datemenu.scss b/home/redyf/desktop/addons/ags/config/style/widgets/datemenu.scss new file mode 100644 index 00000000..0bbc5d28 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/datemenu.scss @@ -0,0 +1,110 @@ +@import "./notifications.scss"; + +@mixin calendar { + @include widget; + padding: $padding*2 $padding*2 0; + + calendar { + all: unset; + + &.button { + @include button($flat: true); + } + + &:selected { + box-shadow: inset 0 -8px 0 0 transparentize($primary-bg, 0.5), + inset 0 0 0 1px $primary-bg; + border-radius: $radius*0.6; + } + + &.header { + background-color: transparent; + border: none; + color: transparentize($fg, 0.5); + } + + &.highlight { + background-color: transparent; + color: transparentize($primary-bg, 0.5); + } + + &:indeterminate { + color: transparentize($fg, 0.9); + } + + font-size: 1.1em; + padding: .2em; + } +} + +window#datemenu .datemenu { + @include floating-widget; + + .notifications { + .header { + margin-bottom: $spacing; + margin-right: $spacing; + + >label { + margin-left: $radius * .5; + } + + button { + @include button; + padding: $padding*.7 $padding; + } + } + + .notification-scrollable { + @include scrollable($top: true, $bottom: true); + } + + .notification-list { + margin-right: $spacing; + } + + .notification { + @include notification; + @include widget; + padding: $padding; + margin-bottom: $spacing; + } + + .placeholder { + image { + font-size: 7em; + } + + label { + font-size: 1.2em; + } + } + } + + + separator { + background-color: $popover-border-color; + border-radius: $radius; + margin-right: $spacing; + } + + .datemenu { + @include spacing; + } + + .clock-box { + padding: $padding; + + .clock { + font-size: 5em; + } + + .uptime { + color: transparentize($fg, 0.2); + } + } + + .calendar { + @include calendar; + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/greeter.scss b/home/redyf/desktop/addons/ags/config/style/widgets/greeter.scss new file mode 100644 index 00000000..0767906d --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/greeter.scss @@ -0,0 +1,64 @@ +@import "../mixins/floating-widget.scss"; +@import "../mixins/widget.scss"; +@import "../mixins/spacing.scss"; +@import "../mixins/unset.scss"; +@import "../mixins/a11y-button.scss"; +@import "./bar.scss"; + +window#greeter { + background-color: lighten($bg, 6%); + color: $fg; + + .bar { + background-color: transparent; + + .date { + @include unset($rec: true); + @include panel-button($flat: true, $reactive: false); + } + } + + .auth { + @include floating_widget; + border-radius: $radius; + min-width: 400px; + padding: 0; + + .wallpaper { + min-height: 220px; + background-size: cover; + border-top-left-radius: $radius; + border-top-right-radius: $radius; + } + + .wallpaper-contrast { + min-height: 100px; + } + + .avatar { + border-radius: 99px; + min-width: 140px; + min-height: 140px; + background-size: cover; + box-shadow: 3px 3px 6px 0 $shadow-color; + margin-bottom: $spacing; + } + + + .password { + entry { + @include button; + padding: $padding*.7 $padding; + margin-left: $spacing*.5; + } + + margin: 0 $padding*4; + margin-top: $spacing; + } + + .response-box { + color: $error-bg; + margin: $spacing 0; + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/launcher.scss b/home/redyf/desktop/addons/ags/config/style/widgets/launcher.scss new file mode 100644 index 00000000..926abc34 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/launcher.scss @@ -0,0 +1,143 @@ +@use "sass:math"; +@use "sass:color"; + +window#launcher .launcher { + @include floating_widget; + + .quicklaunch { + @include spacing; + + button { + @include button($flat: true); + padding: $padding; + } + } + + entry { + @include button; + padding: $padding; + margin: $spacing; + + selection { + color: color.mix($fg, $bg, 50%); + background-color: transparent; + } + + label, + image { + color: $fg; + } + } + + image.spinner { + color: $primary-bg; + margin-right: $spacing; + } + + separator { + margin: 4pt 0; + background-color: $popover-border-color; + } + + button.app-item { + @include button($flat: true, $reactive: false); + + >box { + @include spacing(0.5); + } + + transition: $transition; + padding: $padding; + + label { + transition: $transition; + + &.title { + color: $fg; + } + + &.description { + color: transparentize($fg, 0.3); + } + } + + image { + transition: $transition; + } + + &:hover, + &:focus { + .title { + color: $primary-bg; + } + + .description { + color: transparentize($primary-bg, .4); + } + + image { + -gtk-icon-shadow: 2px 2px $primary-bg; + } + } + + &:active { + background-color: transparentize($primary-bg, 0.5); + border-radius: $radius; + box-shadow: inset 0 0 0 $border-width $border-color; + + .title { + color: $fg; + } + } + } + + button.help, + button.nix-item { + @include button($flat: true, $reactive: false); + padding: 0 ($padding * .5); + + label { + transition: $transition; + color: $fg; + } + + .name { + font-size: 1.2em; + font-weight: bold; + } + + .description { + color: transparentize($fg, .3) + } + + &:hover, + &:focus { + label { + text-shadow: $text-shadow; + } + + .name, + .version { + color: $primary-bg; + } + + .description { + color: transparentize($primary-bg, .3) + } + } + } + + button.sh-item { + @include button($flat: true, $reactive: false); + padding: 0 ($padding * .5); + + transition: $transition; + color: $fg; + + &:hover, + &:focus { + color: $primary-bg; + text-shadow: $text-shadow; + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/notifications.scss b/home/redyf/desktop/addons/ags/config/style/widgets/notifications.scss new file mode 100644 index 00000000..a79d9f2b --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/notifications.scss @@ -0,0 +1,79 @@ +@mixin notification() { + &.critical { + box-shadow: inset 0 0 .5em 0 $error-bg; + } + + &:hover button.close-button { + @include button-hover; + background-color: transparentize($error-bg, .5); + } + + .content { + .title { + margin-right: $spacing; + color: $fg; + font-size: 1.1em; + } + + .time { + color: transparentize($fg, .2); + } + + .description { + font-size: .9em; + color: transparentize($fg, .2); + } + + .icon { + border-radius: $radius*0.8; + margin-right: $spacing; + + &.img { + border: $border; + } + } + } + + box.actions { + @include spacing(0.5); + margin-top: $spacing; + + button { + @include button; + border-radius: $radius*0.8; + font-size: 1.2em; + padding: $padding * 0.7; + } + } + + button.close-button { + @include button($flat: true); + margin-left: $spacing / 2; + border-radius: $radius*0.8; + min-width: 1.2em; + min-height: 1.2em; + + &:hover { + background-color: transparentize($error-bg, .2); + } + + &:active { + background-image: none; + background-color: $error-bg; + } + } +} + +window.notifications { + @include unset; + + .notification { + @include notification; + @include floating-widget; + border-radius: $radius; + + .description { + min-width: 350px; + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/osd.scss b/home/redyf/desktop/addons/ags/config/style/widgets/osd.scss new file mode 100644 index 00000000..111a4861 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/osd.scss @@ -0,0 +1,26 @@ +window.indicator { + .progress { + @include floating-widget; + padding: $padding * .5; + border-radius: if($radius >0, calc($radius + $padding*.5), 0); + @debug $radius; + + .fill { + border-radius: $radius; + background-color: $primary-bg; + color: $primary-fg; + + image { + -gtk-icon-transform: scale(0.7); + } + } + } + + .microphone { + @include floating-widget; + margin: $spacing * 2; + padding: $popover-padding * 2; + font-size: 58px; + color: transparentize($fg, .1) + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/overview.scss b/home/redyf/desktop/addons/ags/config/style/widgets/overview.scss new file mode 100644 index 00000000..c0c4b139 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/overview.scss @@ -0,0 +1,34 @@ +window#overview .overview { + @include floating-widget; + @include spacing; + + .workspace { + &.active>widget { + border-color: $primary-bg + } + + >widget { + @include widget; + border-radius: if($radius ==0, 0, $radius + $padding); + + &:hover { + background-color: $hover-bg; + } + + &:drop(active) { + border-color: $primary-bg; + } + } + } + + .client { + @include button; + border-radius: $radius; + margin: $padding; + + &.hidden { + @include hidden; + transition: 0; + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/powermenu.scss b/home/redyf/desktop/addons/ags/config/style/widgets/powermenu.scss new file mode 100644 index 00000000..d5ce0de1 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/powermenu.scss @@ -0,0 +1,110 @@ +window#powermenu, +window#verification { + // the fraction has to be more than hyprland ignorealpha + background-color: rgba(0, 0, 0, .4); +} + +window#verification .verification { + @include floating-widget; + padding: $popover-padding * 1.5; + min-width: 300px; + min-height: 100px; + + .text-box { + margin-bottom: $spacing; + + .title { + font-size: 1.6em; + } + + .desc { + color: transparentize($fg, 0.1); + font-size: 1.1em; + } + } + + .buttons { + @include spacing; + margin-top: $padding; + + button { + @include button; + font-size: 1.5em; + padding: $padding; + } + } +} + +window#powermenu .powermenu { + @include floating-widget; + + &.line { + padding: $popover-padding * 1.5; + + button { + padding: $popover-padding; + } + + label { + margin-bottom: $spacing * -.5; + } + } + + &.box { + padding: $popover-padding * 2; + + button { + padding: $popover-padding * 1.5; + } + + label { + margin-bottom: $spacing * -1; + } + } + + button { + @include unset; + + image { + @include button; + border-radius: $radius + ($popover-padding * 1.4); + min-width: 1.7em; + min-height: 1.7em; + font-size: 4em; + } + + label, + image { + color: transparentize($fg, 0.1); + } + + label { + margin-top: $spacing * .3; + } + + &:hover { + image { + @include button-hover; + } + + label { + color: $fg; + } + } + + &:focus image { + @include button-focus; + } + + &:active image { + @include button-active; + } + + &:focus, + &:active { + label { + color: $primary-bg; + } + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/quicksettings.scss b/home/redyf/desktop/addons/ags/config/style/widgets/quicksettings.scss new file mode 100644 index 00000000..bd18ff16 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/quicksettings.scss @@ -0,0 +1,177 @@ +window#quicksettings .quicksettings { + @include floating-widget; + @include spacing; + + padding: $popover-padding * 1.4; + + .avatar { + @include widget; + border-radius: $radius * 3; + } + + .header { + @include spacing(.5); + color: transparentize($fg, .15); + + button { + @include button; + padding: $padding; + + image { + font-size: 1.4em; + } + } + } + + .sliders-box { + @include widget; + padding: $padding; + + button { + @include button($flat: true); + padding: $padding * .5; + } + + .volume button.arrow:last-child { + margin-left: $spacing * .4; + } + + .volume, + .brightness { + padding: $padding * .5; + } + + scale { + @include slider; + margin: 0 ($spacing * .5); + + &.muted highlight { + background-image: none; + background-color: transparentize($fg, $amount: .2); + } + } + } + + .row { + @include spacing; + } + + .menu { + @include unset; + @include widget; + padding: $padding; + margin-top: $spacing; + + .icon { + margin: 0 ($spacing * .5); + margin-left: $spacing * .2; + } + + .title { + font-weight: bold; + } + + separator { + margin: ($radius * .5); + background-color: $border-color; + } + + button { + @include button($flat: true); + padding: ($padding * .5); + + image:first-child { + margin-right: $spacing * .5; + } + } + + .bluetooth-devices { + @include spacing(.5); + } + + switch { + @include switch; + } + } + + .sliders-box .menu { + margin: ($spacing * .5) 0; + + &.app-mixer { + .mixer-item { + padding: $padding * .5; + padding-left: 0; + padding-right: $padding * 2; + + scale { + @include slider($width: .5em); + } + + image { + font-size: 1.2em; + margin: 0 $padding; + } + } + } + } + + .toggle-button { + @include button; + font-weight: bold; + + image { + font-size: 1.3em; + } + + label { + margin-left: $spacing * .3; + } + + button { + @include button($flat: true); + + &:first-child { + padding: $padding * 1.2; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:last-child { + padding: $padding * .5; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + } + + &.active { + background-color: $primary-bg; + + label, + image { + color: $primary-fg; + } + } + } + + .simple-toggle { + @include button; + font-weight: bold; + padding: $padding * 1.2; + + label { + margin-left: $spacing * .3; + } + + image { + font-size: 1.3em; + } + } + + .media { + @include spacing; + + .player { + @include media; + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/screencorner.scss b/home/redyf/desktop/addons/ags/config/style/widgets/screencorner.scss new file mode 100644 index 00000000..696fbba3 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/screencorner.scss @@ -0,0 +1,52 @@ +$_shadow-size: $padding; +$_radius: $radius * $hyprland-gaps-multiplier; +$_margin: 99px; + +window.screen-corner:not(.hidden) { + transition: $transition; + + box.shadow { + margin-right: $_margin * -1; + margin-left: $_margin * -1; + + @if $shadows { + box-shadow: inset 0 0 $_shadow-size 0 $shadow-color; + } + + @if $bar-position =="top" { + margin-bottom: $_margin * -1; + } + + @if $bar-position =="bottom" { + margin-top: $_margin * -1; + } + } + + box.border { + @if $bar-position =="top" { + border-top: $border-width solid $bg; + } + + @if $bar-position =="bottom" { + border-bottom: $border-width solid $bg; + } + + margin-right: $_margin; + margin-left: $_margin; + } + + box.corner { + box-shadow: 0 0 0 $border-width $border-color; + } + + &.corners { + box.border { + border-radius: if($radius>0, $radius * $hyprland-gaps-multiplier, 0); + box-shadow: 0 0 0 $_radius $bg; + } + + box.corner { + border-radius: if($radius>0, $radius * $hyprland-gaps-multiplier, 0); + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/style/widgets/settingsdialog.scss b/home/redyf/desktop/addons/ags/config/style/widgets/settingsdialog.scss new file mode 100644 index 00000000..b8c9820d --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/style/widgets/settingsdialog.scss @@ -0,0 +1,144 @@ +window.settings-dialog { + background-color: $bg; + color: $fg; + + .header { + .pager { + @include spacing(.5); + } + + padding: $padding; + + button { + @include button; + font-weight: bold; + padding: $padding*.5 $padding; + + box { + @include spacing($spacing: .3em); + } + } + + button.close { + padding: $padding * .5; + } + + button.reset { + @include button($flat: true); + padding: $padding*.5; + } + } + + .page { + @include scrollable($top: true); + + .page-content { + padding: $padding*2; + padding-top: 0; + } + } + + .group { + .group-title { + color: $primary-bg; + margin-bottom: $spacing*.5; + } + + .group-reset { + @include button($flat: true); + margin: $spacing * .5; + padding: $padding * .5; + + &:disabled { + color: transparent; + } + } + + &:not(:first-child) { + margin-top: $spacing; + } + } + + .row { + background-color: $widget-bg; + padding: $padding; + border: $border; + border-top: none; + + &:first-child { + border-radius: $radius $radius 0 0; + border: $border; + } + + &:last-child { + border-radius: 0 0 $radius $radius; + } + + &:first-child:last-child { + border-radius: $radius; + border: $border; + } + + button.reset { + margin-left: $spacing; + } + + label.id, + label.note { + color: transparentize($fg, .4) + } + + entry, + button { + @include button; + padding: $padding; + } + + switch { + @include switch; + } + + spinbutton { + @include unset; + + entry { + border-radius: $radius 0 0 $radius; + } + + button { + border-radius: 0; + } + + button:last-child { + border-radius: 0 $radius $radius 0; + } + } + + .enum-setter { + label { + background-color: $widget-bg; + border: $border; + padding: 0 $padding; + border-radius: $radius 0 0 $radius; + } + + button { + border-radius: 0; + } + + button:last-child { + border-radius: 0 $radius $radius 0; + } + } + + &.wallpaper { + button { + margin-top: $spacing * .5; + } + + .preview { + border-radius: $radius; + } + } + } +} diff --git a/home/redyf/desktop/addons/ags/config/tsconfig.json b/home/redyf/desktop/addons/ags/config/tsconfig.json new file mode 100644 index 00000000..1708aa31 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": [ + "ES2022" + ], + "allowJs": true, + "checkJs": true, + "strict": true, + "noImplicitAny": false, + "baseUrl": ".", + "typeRoots": [ + "./types", + "./node_modules/@girs" + ], + "skipLibCheck": true + } +} diff --git a/home/redyf/desktop/addons/ags/config/widget/PopupWindow.ts b/home/redyf/desktop/addons/ags/config/widget/PopupWindow.ts new file mode 100644 index 00000000..b53b6fdf --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/PopupWindow.ts @@ -0,0 +1,156 @@ +import { type WindowProps } from "types/widgets/window" +import { type RevealerProps } from "types/widgets/revealer" +import { type EventBoxProps } from "types/widgets/eventbox" +import type Gtk from "gi://Gtk?version=3.0" +import options from "options" + +type Transition = RevealerProps["transition"] +type Child = WindowProps["child"] + +type PopupWindowProps = Omit & { + name: string + layout?: keyof ReturnType + transition?: Transition, +} + +export const Padding = (name: string, { + css = "", + hexpand = true, + vexpand = true, +}: EventBoxProps = {}) => Widget.EventBox({ + hexpand, + vexpand, + can_focus: false, + child: Widget.Box({ css }), + setup: w => w.on("button-press-event", () => App.toggleWindow(name)), +}) + +const PopupRevealer = ( + name: string, + child: Child, + transition: Transition = "slide_down", +) => Widget.Box( + { css: "padding: 1px;" }, + Widget.Revealer({ + transition, + child: Widget.Box({ + class_name: "window-content", + child, + }), + transitionDuration: options.transition.bind(), + setup: self => self.hook(App, (_, wname, visible) => { + if (wname === name) + self.reveal_child = visible + }), + }), +) + +const Layout = (name: string, child: Child, transition?: Transition) => ({ + "center": () => Widget.CenterBox({}, + Padding(name), + Widget.CenterBox( + { vertical: true }, + Padding(name), + PopupRevealer(name, child, transition), + Padding(name), + ), + Padding(name), + ), + "top": () => Widget.CenterBox({}, + Padding(name), + Widget.Box( + { vertical: true }, + PopupRevealer(name, child, transition), + Padding(name), + ), + Padding(name), + ), + "top-right": () => Widget.Box({}, + Padding(name), + Widget.Box( + { + hexpand: false, + vertical: true, + }, + PopupRevealer(name, child, transition), + Padding(name), + ), + ), + "top-center": () => Widget.Box({}, + Padding(name), + Widget.Box( + { + hexpand: false, + vertical: true, + }, + PopupRevealer(name, child, transition), + Padding(name), + ), + Padding(name), + ), + "top-left": () => Widget.Box({}, + Widget.Box( + { + hexpand: false, + vertical: true, + }, + PopupRevealer(name, child, transition), + Padding(name), + ), + Padding(name), + ), + "bottom-left": () => Widget.Box({}, + Widget.Box( + { + hexpand: false, + vertical: true, + }, + Padding(name), + PopupRevealer(name, child, transition), + ), + Padding(name), + ), + "bottom-center": () => Widget.Box({}, + Padding(name), + Widget.Box( + { + hexpand: false, + vertical: true, + }, + Padding(name), + PopupRevealer(name, child, transition), + ), + Padding(name), + ), + "bottom-right": () => Widget.Box({}, + Padding(name), + Widget.Box( + { + hexpand: false, + vertical: true, + }, + Padding(name), + PopupRevealer(name, child, transition), + ), + ), +}) + +export default ({ + name, + child, + layout = "center", + transition, + exclusivity = "ignore", + ...props +}: PopupWindowProps) => Widget.Window({ + name, + class_names: [name, "popup-window"], + setup: w => w.keybind("Escape", () => App.closeWindow(name)), + visible: false, + keymode: "on-demand", + exclusivity, + layer: "top", + anchor: ["top", "bottom", "right", "left"], + child: Layout(name, child, transition)[layout](), + ...props, +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/RegularWindow.ts b/home/redyf/desktop/addons/ags/config/widget/RegularWindow.ts new file mode 100644 index 00000000..1e4225dd --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/RegularWindow.ts @@ -0,0 +1,3 @@ +import Gtk from "gi://Gtk?version=3.0" + +export default Widget.subclass(Gtk.Window) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/Bar.ts b/home/redyf/desktop/addons/ags/config/widget/bar/Bar.ts new file mode 100644 index 00000000..99707574 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/Bar.ts @@ -0,0 +1,60 @@ +import BatteryBar from "./buttons/BatteryBar" +import ColorPicker from "./buttons/ColorPicker" +import Date from "./buttons/Date" +import Launcher from "./buttons/Launcher" +import Media from "./buttons/Media" +import PowerMenu from "./buttons/PowerMenu" +import SysTray from "./buttons/SysTray" +import SystemIndicators from "./buttons/SystemIndicators" +import Taskbar from "./buttons/Taskbar" +import Workspaces from "./buttons/Workspaces" +import ScreenRecord from "./buttons/ScreenRecord" +import Messages from "./buttons/Messages" +import options from "options" + +const { start, center, end } = options.bar.layout +const { transparent, position } = options.bar + +export type BarWidget = keyof typeof widget + +const widget = { + battery: BatteryBar, + colorpicker: ColorPicker, + date: Date, + launcher: Launcher, + media: Media, + powermenu: PowerMenu, + systray: SysTray, + system: SystemIndicators, + taskbar: Taskbar, + workspaces: Workspaces, + screenrecord: ScreenRecord, + messages: Messages, + expander: () => Widget.Box({ expand: true }), +} + +export default (monitor: number) => Widget.Window({ + monitor, + class_name: "bar", + name: `bar${monitor}`, + exclusivity: "exclusive", + anchor: position.bind().as(pos => [pos, "right", "left"]), + child: Widget.CenterBox({ + css: "min-width: 2px; min-height: 2px;", + startWidget: Widget.Box({ + hexpand: true, + children: start.bind().as(s => s.map(w => widget[w]())), + }), + centerWidget: Widget.Box({ + hpack: "center", + children: center.bind().as(c => c.map(w => widget[w]())), + }), + endWidget: Widget.Box({ + hexpand: true, + children: end.bind().as(e => e.map(w => widget[w]())), + }), + }), + setup: self => self.hook(transparent, () => { + self.toggleClassName("transparent", transparent.value) + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/PanelButton.ts b/home/redyf/desktop/addons/ags/config/widget/bar/PanelButton.ts new file mode 100644 index 00000000..1e5fafce --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/PanelButton.ts @@ -0,0 +1,46 @@ +import options from "options" +import { ButtonProps } from "types/widgets/button" + +type PanelButtonProps = ButtonProps & { + window?: string, + flat?: boolean +} + +export default ({ + window = "", + flat, + child, + setup, + ...rest +}: PanelButtonProps) => Widget.Button({ + child: Widget.Box({ child }), + setup: self => { + let open = false + + self.toggleClassName("panel-button") + self.toggleClassName(window) + + self.hook(options.bar.flatButtons, () => { + self.toggleClassName("flat", flat ?? options.bar.flatButtons.value) + }) + + self.hook(App, (_, win, visible) => { + if (win !== window) + return + + if (open && !visible) { + open = false + self.toggleClassName("active", false) + } + + if (visible) { + open = true + self.toggleClassName("active") + } + }) + + if (setup) + setup(self) + }, + ...rest, +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/ScreenCorners.ts b/home/redyf/desktop/addons/ags/config/widget/bar/ScreenCorners.ts new file mode 100644 index 00000000..8d7c52d5 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/ScreenCorners.ts @@ -0,0 +1,29 @@ +import options from "options" + +const { corners, transparent } = options.bar + +export default (monitor: number) => Widget.Window({ + monitor, + name: `corner${monitor}`, + class_name: "screen-corner", + anchor: ["top", "bottom", "right", "left"], + click_through: true, + child: Widget.Box({ + class_name: "shadow", + child: Widget.Box({ + class_name: "border", + expand: true, + child: Widget.Box({ + class_name: "corner", + expand: true, + }), + }), + }), + setup: self => self + .hook(corners, () => { + self.toggleClassName("corners", corners.value) + }) + .hook(transparent, () => { + self.toggleClassName("hidden", transparent.value) + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/BatteryBar.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/BatteryBar.ts new file mode 100644 index 00000000..18de3292 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/BatteryBar.ts @@ -0,0 +1,94 @@ +import icons from "lib/icons" +import options from "options" +import PanelButton from "../PanelButton" + +const battery = await Service.import("battery") +const { bar, percentage, blocks, width, low } = options.bar.battery + +const Indicator = () => Widget.Icon({ + setup: self => self.hook(battery, () => { + self.icon = battery.charging || battery.charged + ? icons.battery.charging + : battery.icon_name + }), +}) + +const PercentLabel = () => Widget.Revealer({ + transition: "slide_right", + click_through: true, + reveal_child: percentage.bind(), + child: Widget.Label({ + label: battery.bind("percent").as(p => `${p}%`), + }), +}) + +const LevelBar = () => { + const level = Widget.LevelBar({ + bar_mode: "discrete", + max_value: blocks.bind(), + visible: bar.bind().as(b => b !== "hidden"), + value: battery.bind("percent").as(p => (p / 100) * blocks.value), + }) + const update = () => { + level.value = (battery.percent / 100) * blocks.value + level.css = `block { min-width: ${width.value / blocks.value}pt; }` + } + return level + .hook(width, update) + .hook(blocks, update) + .hook(bar, () => { + level.vpack = bar.value === "whole" ? "fill" : "center" + level.hpack = bar.value === "whole" ? "fill" : "center" + }) +} + +const WholeButton = () => Widget.Overlay({ + vexpand: true, + child: LevelBar(), + class_name: "whole", + pass_through: true, + overlay: Widget.Box({ + hpack: "center", + children: [ + Widget.Icon({ + icon: icons.battery.charging, + visible: Utils.merge([ + battery.bind("charging"), + battery.bind("charged"), + ], (ing, ed) => ing || ed), + }), + Widget.Box({ + hpack: "center", + vpack: "center", + child: PercentLabel(), + }), + ], + }), +}) + +const Regular = () => Widget.Box({ + class_name: "regular", + children: [ + Indicator(), + PercentLabel(), + LevelBar(), + ], +}) + +export default () => PanelButton({ + class_name: "battery-bar", + hexpand: false, + on_clicked: () => { percentage.value = !percentage.value }, + visible: battery.bind("available"), + child: Widget.Box({ + expand: true, + visible: battery.bind("available"), + child: bar.bind().as(b => b === "whole" ? WholeButton() : Regular()), + }), + setup: self => self + .hook(bar, w => w.toggleClassName("bar-hidden", bar.value === "hidden")) + .hook(battery, w => { + w.toggleClassName("charging", battery.charging || battery.charged) + w.toggleClassName("low", battery.percent < low.value) + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/ColorPicker.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/ColorPicker.ts new file mode 100644 index 00000000..5b1f3f62 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/ColorPicker.ts @@ -0,0 +1,37 @@ +import PanelButton from "../PanelButton" +import colorpicker from "service/colorpicker" +import Gdk from "gi://Gdk" + +const css = (color: string) => ` +* { + background-color: ${color}; + color: transparent; +} +*:hover { + color: white; + text-shadow: 2px 2px 3px rgba(0,0,0,.8); +}` + +export default () => { + const menu = Widget.Menu({ + class_name: "colorpicker", + children: colorpicker.bind("colors").as(c => c.map(color => Widget.MenuItem({ + child: Widget.Label(color), + css: css(color), + on_activate: () => colorpicker.wlCopy(color), + }))), + }) + + return PanelButton({ + class_name: "color-picker", + child: Widget.Icon("color-select-symbolic"), + tooltip_text: colorpicker.bind("colors").as(v => `${v.length} colors`), + on_clicked: colorpicker.pick, + on_secondary_click: self => { + if (colorpicker.colors.length === 0) + return + + menu.popup_at_widget(self, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null) + }, + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Date.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Date.ts new file mode 100644 index 00000000..4c71afb9 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Date.ts @@ -0,0 +1,15 @@ +import { clock } from "lib/variables" +import PanelButton from "../PanelButton" +import options from "options" + +const { format, action } = options.bar.date +const time = Utils.derive([clock, format], (c, f) => c.format(f) || "") + +export default () => PanelButton({ + window: "datemenu", + on_clicked: action.bind(), + child: Widget.Label({ + justification: "center", + label: time.bind(), + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Launcher.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Launcher.ts new file mode 100644 index 00000000..f3fee6bb --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Launcher.ts @@ -0,0 +1,49 @@ +import PanelButton from "../PanelButton" +import options from "options" +import nix from "service/nix" + +const { icon, label, action } = options.bar.launcher + +function Spinner() { + const child = Widget.Icon({ + icon: icon.icon.bind(), + class_name: Utils.merge([ + icon.colored.bind(), + nix.bind("ready"), + ], (c, r) => `${c ? "colored" : ""} ${r ? "" : "spinning"}`), + css: ` + @keyframes spin { + to { -gtk-icon-transform: rotate(1turn); } + } + + image.spinning { + animation-name: spin; + animation-duration: 1s; + animation-timing-function: linear; + animation-iteration-count: infinite; + } + `, + }) + + return Widget.Revealer({ + transition: "slide_left", + child, + reveal_child: Utils.merge([ + icon.icon.bind(), + nix.bind("ready"), + ], (i, r) => Boolean(i || r)), + }) +} + +export default () => PanelButton({ + window: "launcher", + on_clicked: action.bind(), + child: Widget.Box([ + Spinner(), + Widget.Label({ + class_name: label.colored.bind().as(c => c ? "colored" : ""), + visible: label.label.bind().as(v => !!v), + label: label.label.bind(), + }), + ]), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Media.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Media.ts new file mode 100644 index 00000000..b3aab611 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Media.ts @@ -0,0 +1,92 @@ +import { type MprisPlayer } from "types/service/mpris" +import PanelButton from "../PanelButton" +import options from "options" +import icons from "lib/icons" +import { icon } from "lib/utils" + +const mpris = await Service.import("mpris") +const { length, direction, preferred, monochrome, format } = options.bar.media + +const getPlayer = (name = preferred.value) => + mpris.getPlayer(name) || mpris.players[0] || null + +const Content = (player: MprisPlayer) => { + const revealer = Widget.Revealer({ + click_through: true, + visible: length.bind().as(l => l > 0), + transition: direction.bind().as(d => `slide_${d}` as const), + setup: self => { + let current = "" + self.hook(player, () => { + if (current === player.track_title) + return + + current = player.track_title + self.reveal_child = true + Utils.timeout(3000, () => { + !self.is_destroyed && (self.reveal_child = false) + }) + }) + }, + child: Widget.Label({ + truncate: "end", + max_width_chars: length.bind().as(n => n > 0 ? n : -1), + label: Utils.merge([ + player.bind("track_title"), + player.bind("track_artists"), + format.bind(), + ], () => `${format}` + .replace("{title}", player.track_title) + .replace("{artists}", player.track_artists.join(", ")) + .replace("{artist}", player.track_artists[0] || "") + .replace("{album}", player.track_album) + .replace("{name}", player.name) + .replace("{identity}", player.identity), + ), + }), + }) + + const playericon = Widget.Icon({ + icon: Utils.merge([player.bind("entry"), monochrome.bind()], (entry => { + const name = `${entry}${monochrome.value ? "-symbolic" : ""}` + return icon(name, icons.fallback.audio) + })), + }) + + return Widget.Box({ + attribute: { revealer }, + children: direction.bind().as(d => d === "right" + ? [playericon, revealer] : [revealer, playericon]), + }) +} + +export default () => { + let player = getPlayer() + + const btn = PanelButton({ + class_name: "media", + child: Widget.Icon(icons.fallback.audio), + }) + + const update = () => { + player = getPlayer() + btn.visible = !!player + + if (!player) + return + + const content = Content(player) + const { revealer } = content.attribute + btn.child = content + btn.on_primary_click = () => { player.playPause() } + btn.on_secondary_click = () => { player.playPause() } + btn.on_scroll_up = () => { player.next() } + btn.on_scroll_down = () => { player.previous() } + btn.on_hover = () => { revealer.reveal_child = true } + btn.on_hover_lost = () => { revealer.reveal_child = false } + } + + return btn + .hook(preferred, update) + .hook(mpris, update, "notify::players") +} diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Messages.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Messages.ts new file mode 100644 index 00000000..a8971e90 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Messages.ts @@ -0,0 +1,16 @@ +import icons from "lib/icons" +import PanelButton from "../PanelButton" +import options from "options" + +const n = await Service.import("notifications") +const notifs = n.bind("notifications") +const action = options.bar.messages.action.bind() + +export default () => PanelButton({ + class_name: "messages", + on_clicked: action, + visible: notifs.as(n => n.length > 0), + child: Widget.Box([ + Widget.Icon(icons.notifications.message), + ]), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/PowerMenu.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/PowerMenu.ts new file mode 100644 index 00000000..4432adea --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/PowerMenu.ts @@ -0,0 +1,15 @@ +import icons from "lib/icons" +import PanelButton from "../PanelButton" +import options from "options" + +const { monochrome, action } = options.bar.powermenu + +export default () => PanelButton({ + window: "powermenu", + on_clicked: action.bind(), + child: Widget.Icon(icons.powermenu.shutdown), + setup: self => self.hook(monochrome, () => { + self.toggleClassName("colored", !monochrome.value) + self.toggleClassName("box") + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/ScreenRecord.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/ScreenRecord.ts new file mode 100644 index 00000000..1d6eb36e --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/ScreenRecord.ts @@ -0,0 +1,21 @@ +import PanelButton from "../PanelButton" +import screenrecord from "service/screenrecord" +import icons from "lib/icons" + +export default () => PanelButton({ + class_name: "recorder", + on_clicked: () => screenrecord.stop(), + visible: screenrecord.bind("recording"), + child: Widget.Box({ + children: [ + Widget.Icon(icons.recorder.recording), + Widget.Label({ + label: screenrecord.bind("timer").as(time => { + const sec = time % 60 + const min = Math.floor(time / 60) + return `${min}:${sec < 10 ? "0" + sec : sec}` + }), + }), + ], + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/SysTray.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/SysTray.ts new file mode 100644 index 00000000..9f569d1f --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/SysTray.ts @@ -0,0 +1,39 @@ +import { type TrayItem } from "types/service/systemtray" +import PanelButton from "../PanelButton" +import Gdk from "gi://Gdk" +import options from "options" + +const systemtray = await Service.import("systemtray") +const { ignore } = options.bar.systray + +const SysTrayItem = (item: TrayItem) => PanelButton({ + class_name: "tray-item", + child: Widget.Icon({ icon: item.bind("icon") }), + tooltip_markup: item.bind("tooltip_markup"), + setup: self => { + const { menu } = item + if (!menu) + return + + const id = menu.connect("popped-up", () => { + self.toggleClassName("active") + menu.connect("notify::visible", () => { + self.toggleClassName("active", menu.visible) + }) + menu.disconnect(id!) + }) + + self.connect("destroy", () => menu.disconnect(id)) + }, + + on_primary_click: btn => item.menu?.popup_at_widget( + btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null), + + on_secondary_click: btn => item.menu?.popup_at_widget( + btn, Gdk.Gravity.SOUTH, Gdk.Gravity.NORTH, null), +}) + +export default () => Widget.Box() + .bind("children", systemtray, "items", i => i + .filter(({ id }) => !ignore.value.includes(id)) + .map(SysTrayItem)) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/SystemIndicators.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/SystemIndicators.ts new file mode 100644 index 00000000..3fd916d0 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/SystemIndicators.ts @@ -0,0 +1,98 @@ +import PanelButton from "../PanelButton" +import icons from "lib/icons" +import asusctl from "service/asusctl" + +const notifications = await Service.import("notifications") +const bluetooth = await Service.import("bluetooth") +const audio = await Service.import("audio") +const network = await Service.import("network") +const powerprof = await Service.import("powerprofiles") + +const ProfileIndicator = () => { + const visible = asusctl.available + ? asusctl.bind("profile").as(p => p !== "Balanced") + : powerprof.bind("active_profile").as(p => p !== "balanced") + + const icon = asusctl.available + ? asusctl.bind("profile").as(p => icons.asusctl.profile[p]) + : powerprof.bind("active_profile").as(p => icons.powerprofile[p]) + + return Widget.Icon({ visible, icon }) +} + +const ModeIndicator = () => { + if (!asusctl.available) { + return Widget.Icon({ + setup(self) { + Utils.idle(() => self.visible = false) + }, + }) + } + + return Widget.Icon({ + visible: asusctl.bind("mode").as(m => m !== "Hybrid"), + icon: asusctl.bind("mode").as(m => icons.asusctl.mode[m]), + }) +} + +const MicrophoneIndicator = () => Widget.Icon() + .hook(audio, self => self.visible = + audio.recorders.length > 0 + || audio.microphone.is_muted + || false) + .hook(audio.microphone, self => { + const vol = audio.microphone.is_muted ? 0 : audio.microphone.volume + const { muted, low, medium, high } = icons.audio.mic + const cons = [[67, high], [34, medium], [1, low], [0, muted]] as const + self.icon = cons.find(([n]) => n <= vol * 100)?.[1] || "" + }) + +const DNDIndicator = () => Widget.Icon({ + visible: notifications.bind("dnd"), + icon: icons.notifications.silent, +}) + +const BluetoothIndicator = () => Widget.Overlay({ + class_name: "bluetooth", + passThrough: true, + visible: bluetooth.bind("enabled"), + child: Widget.Icon({ + icon: icons.bluetooth.enabled, + }), + overlay: Widget.Label({ + hpack: "end", + vpack: "start", + label: bluetooth.bind("connected_devices").as(c => `${c.length}`), + visible: bluetooth.bind("connected_devices").as(c => c.length > 0), + }), +}) + +const NetworkIndicator = () => Widget.Icon().hook(network, self => { + const icon = network[network.primary || "wifi"]?.icon_name + self.icon = icon || "" + self.visible = !!icon +}) + +const AudioIndicator = () => Widget.Icon() + .hook(audio.speaker, self => { + const vol = audio.speaker.is_muted ? 0 : audio.speaker.volume + const { muted, low, medium, high, overamplified } = icons.audio.volume + const cons = [[101, overamplified], [67, high], [34, medium], [1, low], [0, muted]] as const + self.icon = cons.find(([n]) => n <= vol * 100)?.[1] || "" + }) + +export default () => PanelButton({ + window: "quicksettings", + on_clicked: () => App.toggleWindow("quicksettings"), + on_scroll_up: () => audio.speaker.volume += 0.02, + on_scroll_down: () => audio.speaker.volume -= 0.02, + child: Widget.Box([ + ProfileIndicator(), + ModeIndicator(), + DNDIndicator(), + BluetoothIndicator(), + NetworkIndicator(), + AudioIndicator(), + MicrophoneIndicator(), + ]), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Taskbar.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Taskbar.ts new file mode 100644 index 00000000..b9c65fa7 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Taskbar.ts @@ -0,0 +1,90 @@ +import { launchApp, icon } from "lib/utils" +import icons from "lib/icons" +import options from "options" +import PanelButton from "../PanelButton" + +const hyprland = await Service.import("hyprland") +const apps = await Service.import("applications") +const { monochrome, exclusive, iconSize } = options.bar.taskbar +const { position } = options.bar + +const focus = (address: string) => hyprland.messageAsync( + `dispatch focuswindow address:${address}`) + +const DummyItem = (address: string) => Widget.Box({ + attribute: { address }, + visible: false, +}) + +const AppItem = (address: string) => { + const client = hyprland.getClient(address) + if (!client || client.class === "") + return DummyItem(address) + + const app = apps.list.find(app => app.match(client.class)) + + const btn = PanelButton({ + class_name: "panel-button", + tooltip_text: Utils.watch(client.title, hyprland, () => + hyprland.getClient(address)?.title || "", + ), + on_primary_click: () => focus(address), + on_middle_click: () => app && launchApp(app), + child: Widget.Icon({ + size: iconSize.bind(), + icon: monochrome.bind().as(m => icon( + (app?.icon_name || client.class) + (m ? "-symbolic" : ""), + icons.fallback.executable + (m ? "-symbolic" : ""), + )), + }), + }) + + return Widget.Box( + { + attribute: { address }, + visible: Utils.watch(true, [exclusive, hyprland], () => { + return exclusive.value + ? hyprland.active.workspace.id === client.workspace.id + : true + }), + }, + Widget.Overlay({ + child: btn, + pass_through: true, + overlay: Widget.Box({ + className: "indicator", + hpack: "center", + vpack: position.bind().as(p => p === "top" ? "start" : "end"), + setup: w => w.hook(hyprland, () => { + w.toggleClassName("active", hyprland.active.client.address === address) + }), + }), + }), + ) +} + +function sortItems(arr: T[]) { + return arr.sort(({ attribute: a }, { attribute: b }) => { + const aclient = hyprland.getClient(a.address)! + const bclient = hyprland.getClient(b.address)! + return aclient.workspace.id - bclient.workspace.id + }) +} + +export default () => Widget.Box({ + class_name: "taskbar", + children: sortItems(hyprland.clients.map(c => AppItem(c.address))), + setup: w => w + .hook(hyprland, (w, address?: string) => { + if (typeof address === "string") + w.children = w.children.filter(ch => ch.attribute.address !== address) + }, "client-removed") + .hook(hyprland, (w, address?: string) => { + if (typeof address === "string") + w.children = sortItems([...w.children, AppItem(address)]) + }, "client-added") + .hook(hyprland, (w, event?: string) => { + if (event === "movewindow") + w.children = sortItems(w.children) + }, "event"), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Workspaces.ts b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Workspaces.ts new file mode 100644 index 00000000..73ea3472 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/bar/buttons/Workspaces.ts @@ -0,0 +1,38 @@ +import PanelButton from "../PanelButton" +import options from "options" +import { sh, range } from "lib/utils" + +const hyprland = await Service.import("hyprland") +const { workspaces } = options.bar.workspaces + +const dispatch = (arg: string | number) => { + sh(`hyprctl dispatch workspace ${arg}`) +} + +const Workspaces = (ws: number) => Widget.Box({ + children: range(ws || 20).map(i => Widget.Label({ + attribute: i, + vpack: "center", + label: `${i}`, + setup: self => self.hook(hyprland, () => { + self.toggleClassName("active", hyprland.active.workspace.id === i) + self.toggleClassName("occupied", (hyprland.getWorkspace(i)?.windows || 0) > 0) + }), + })), + setup: box => { + if (ws === 0) { + box.hook(hyprland.active.workspace, () => box.children.map(btn => { + btn.visible = hyprland.workspaces.some(ws => ws.id === btn.attribute) + })) + } + }, +}) + +export default () => PanelButton({ + window: "overview", + class_name: "workspaces", + on_scroll_up: () => dispatch("m+1"), + on_scroll_down: () => dispatch("m-1"), + on_clicked: () => App.toggleWindow("overview"), + child: workspaces.bind().as(Workspaces), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/datemenu/DateColumn.ts b/home/redyf/desktop/addons/ags/config/widget/datemenu/DateColumn.ts new file mode 100644 index 00000000..94e70519 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/datemenu/DateColumn.ts @@ -0,0 +1,37 @@ +import { clock, uptime } from "lib/variables" + +function up(up: number) { + const h = Math.floor(up / 60) + const m = Math.floor(up % 60) + return `uptime: ${h}:${m < 10 ? "0" + m : m}` +} + +export default () => Widget.Box({ + vertical: true, + class_name: "date-column vertical", + children: [ + Widget.Box({ + class_name: "clock-box", + vertical: true, + children: [ + Widget.Label({ + class_name: "clock", + label: clock.bind().as(t => t.format("%H:%M")!), + }), + Widget.Label({ + class_name: "uptime", + label: uptime.bind().as(up), + }), + ], + }), + Widget.Box({ + class_name: "calendar", + children: [ + Widget.Calendar({ + hexpand: true, + hpack: "center", + }), + ], + }), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/datemenu/DateMenu.ts b/home/redyf/desktop/addons/ags/config/widget/datemenu/DateMenu.ts new file mode 100644 index 00000000..f7fdf6d9 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/datemenu/DateMenu.ts @@ -0,0 +1,36 @@ +import PopupWindow from "widget/PopupWindow" +import NotificationColumn from "./NotificationColumn" +import DateColumn from "./DateColumn" +import options from "options" + +const { bar, datemenu } = options +const pos = bar.position.bind() +const layout = Utils.derive([bar.position, datemenu.position], (bar, qs) => + `${bar}-${qs}` as const, +) + +const Settings = () => Widget.Box({ + class_name: "datemenu horizontal", + vexpand: false, + children: [ + NotificationColumn(), + Widget.Separator({ orientation: 1 }), + DateColumn(), + ], +}) + +const DateMenu = () => PopupWindow({ + name: "datemenu", + exclusivity: "exclusive", + transition: pos.as(pos => pos === "top" ? "slide_down" : "slide_up"), + layout: layout.value, + child: Settings(), +}) + +export function setupDateMenu() { + App.addWindow(DateMenu()) + layout.connect("changed", () => { + App.removeWindow("datemenu") + App.addWindow(DateMenu()) + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/datemenu/NotificationColumn.ts b/home/redyf/desktop/addons/ags/config/widget/datemenu/NotificationColumn.ts new file mode 100644 index 00000000..07d6829c --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/datemenu/NotificationColumn.ts @@ -0,0 +1,113 @@ +import { type Notification as Notif } from "types/service/notifications" +import Notification from "widget/notifications/Notification" +import options from "options" +import icons from "lib/icons" + +const notifications = await Service.import("notifications") +const notifs = notifications.bind("notifications") + +const Animated = (n: Notif) => Widget.Revealer({ + transition_duration: options.transition.value, + transition: "slide_down", + child: Notification(n), + setup: self => Utils.timeout(options.transition.value, () => { + if (!self.is_destroyed) + self.reveal_child = true + }), +}) + +const ClearButton = () => Widget.Button({ + on_clicked: notifications.clear, + sensitive: notifs.as(n => n.length > 0), + child: Widget.Box({ + children: [ + Widget.Label("Clear "), + Widget.Icon({ + icon: notifs.as(n => icons.trash[n.length > 0 ? "full" : "empty"]), + }), + ], + }), +}) + +const Header = () => Widget.Box({ + class_name: "header", + children: [ + Widget.Label({ label: "Notifications", hexpand: true, xalign: 0 }), + ClearButton(), + ], +}) + +const NotificationList = () => { + const map: Map> = new Map + const box = Widget.Box({ + vertical: true, + children: notifications.notifications.map(n => { + const w = Animated(n) + map.set(n.id, w) + return w + }), + visible: notifs.as(n => n.length > 0), + }) + + function remove(_: unknown, id: number) { + const n = map.get(id) + if (n) { + n.reveal_child = false + Utils.timeout(options.transition.value, () => { + n.destroy() + map.delete(id) + }) + } + } + + return box + .hook(notifications, remove, "closed") + .hook(notifications, (_, id: number) => { + if (id !== undefined) { + if (map.has(id)) + remove(null, id) + + const n = notifications.getNotification(id)! + + const w = Animated(n) + map.set(id, w) + box.children = [w, ...box.children] + } + }, "notified") +} + +const Placeholder = () => Widget.Box({ + class_name: "placeholder", + vertical: true, + vpack: "center", + hpack: "center", + vexpand: true, + hexpand: true, + visible: notifs.as(n => n.length === 0), + children: [ + Widget.Icon(icons.notifications.silent), + Widget.Label("Your inbox is empty"), + ], +}) + +export default () => Widget.Box({ + class_name: "notifications", + css: options.notifications.width.bind().as(w => `min-width: ${w}px`), + vertical: true, + children: [ + Header(), + Widget.Scrollable({ + vexpand: true, + hscroll: "never", + class_name: "notification-scrollable", + child: Widget.Box({ + class_name: "notification-list vertical", + vertical: true, + children: [ + NotificationList(), + Placeholder(), + ], + }), + }), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/desktop/Desktop.ts b/home/redyf/desktop/addons/ags/config/widget/desktop/Desktop.ts new file mode 100644 index 00000000..f7119677 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/desktop/Desktop.ts @@ -0,0 +1,40 @@ +import options from "options" +import { matugen } from "lib/matugen" +const mpris = await Service.import("mpris") + +const pref = () => options.bar.media.preferred.value + +export default (monitor: number) => Widget.Window({ + monitor, + layer: "bottom", + name: `desktop${monitor}`, + class_name: "desktop", + anchor: ["top", "bottom", "left", "right"], + child: Widget.Box({ + expand: true, + css: options.theme.dark.primary.bg.bind().as(c => ` + transition: 500ms; + background-color: ${c}`), + child: Widget.Box({ + class_name: "wallpaper", + expand: true, + vpack: "center", + hpack: "center", + setup: self => self + .hook(mpris, () => { + const img = mpris.getPlayer(pref())!.cover_path + matugen("image", img) + Utils.timeout(500, () => self.css = ` + background-image: url('${img}'); + background-size: contain; + background-repeat: no-repeat; + transition: 200ms; + min-width: 700px; + min-height: 700px; + border-radius: 30px; + box-shadow: 25px 25px 30px 0 rgba(0,0,0,0.5);`, + ) + }), + }), + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/launcher/AppLauncher.ts b/home/redyf/desktop/addons/ags/config/widget/launcher/AppLauncher.ts new file mode 100644 index 00000000..131fc858 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/launcher/AppLauncher.ts @@ -0,0 +1,130 @@ +import { type Application } from "types/service/applications" +import { launchApp, icon } from "lib/utils" +import options from "options" +import icons from "lib/icons" + +const apps = await Service.import("applications") +const { query } = apps +const { iconSize } = options.launcher.apps + +const QuickAppButton = (app: Application) => Widget.Button({ + hexpand: true, + tooltip_text: app.name, + on_clicked: () => { + App.closeWindow("launcher") + launchApp(app) + }, + child: Widget.Icon({ + size: iconSize.bind(), + icon: icon(app.icon_name, icons.fallback.executable), + }), +}) + +const AppItem = (app: Application) => { + const title = Widget.Label({ + class_name: "title", + label: app.name, + hexpand: true, + xalign: 0, + vpack: "center", + truncate: "end", + }) + + const description = Widget.Label({ + class_name: "description", + label: app.description || "", + hexpand: true, + wrap: true, + max_width_chars: 30, + xalign: 0, + justification: "left", + vpack: "center", + }) + + const appicon = Widget.Icon({ + icon: icon(app.icon_name, icons.fallback.executable), + size: iconSize.bind(), + }) + + const textBox = Widget.Box({ + vertical: true, + vpack: "center", + children: app.description ? [title, description] : [title], + }) + + return Widget.Button({ + class_name: "app-item", + attribute: { app }, + child: Widget.Box({ + children: [appicon, textBox], + }), + on_clicked: () => { + App.closeWindow("launcher") + launchApp(app) + }, + }) +} +export function Favorites() { + const favs = options.launcher.apps.favorites.bind() + return Widget.Revealer({ + visible: favs.as(f => f.length > 0), + child: Widget.Box({ + vertical: true, + children: favs.as(favs => favs.flatMap(fs => [ + Widget.Separator(), + Widget.Box({ + class_name: "quicklaunch horizontal", + children: fs + .map(f => query(f)?.[0]) + .filter(f => f) + .map(QuickAppButton), + }), + ])), + }), + }) +} + +export function Launcher() { + const applist = Variable(query("")) + const max = options.launcher.apps.max + let first = applist.value[0] + + function SeparatedAppItem(app: Application) { + return Widget.Revealer( + { attribute: { app } }, + Widget.Box( + { vertical: true }, + Widget.Separator(), + AppItem(app), + ), + ) + } + + const list = Widget.Box({ + vertical: true, + children: applist.bind().as(list => list.map(SeparatedAppItem)), + setup: self => self + .hook(apps, () => applist.value = query(""), "notify::frequents"), + }) + + return Object.assign(list, { + filter(text: string | null) { + first = query(text || "")[0] + list.children.reduce((i, item) => { + if (!text || i >= max.value) { + item.reveal_child = false + return i + } + if (item.attribute.app.match(text)) { + item.reveal_child = true + return ++i + } + item.reveal_child = false + return i + }, 0) + }, + launchFirst() { + launchApp(first) + }, + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/launcher/Launcher.ts b/home/redyf/desktop/addons/ags/config/widget/launcher/Launcher.ts new file mode 100644 index 00000000..da33dd13 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/launcher/Launcher.ts @@ -0,0 +1,139 @@ +import { type Binding } from "lib/utils" +import PopupWindow, { Padding } from "widget/PopupWindow" +import icons from "lib/icons" +import options from "options" +import nix from "service/nix" +import * as AppLauncher from "./AppLauncher" +import * as NixRun from "./NixRun" +import * as ShRun from "./ShRun" + +const { width, margin } = options.launcher +const isnix = nix.available + +function Launcher() { + const favs = AppLauncher.Favorites() + const applauncher = AppLauncher.Launcher() + const sh = ShRun.ShRun() + const shicon = ShRun.Icon() + const nix = NixRun.NixRun() + const nixload = NixRun.Spinner() + + function HelpButton(cmd: string, desc: string | Binding) { + return Widget.Box( + { vertical: true }, + Widget.Separator(), + Widget.Button( + { + class_name: "help", + on_clicked: () => { + entry.grab_focus() + entry.text = `:${cmd} ` + entry.set_position(-1) + }, + }, + Widget.Box([ + Widget.Label({ + class_name: "name", + label: `:${cmd}`, + }), + Widget.Label({ + hexpand: true, + hpack: "end", + class_name: "description", + label: desc, + }), + ]), + ), + ) + } + + const help = Widget.Revealer({ + child: Widget.Box( + { vertical: true }, + HelpButton("sh", "run a binary"), + isnix ? HelpButton("nx", options.launcher.nix.pkgs.bind().as(pkg => + `run a nix package from ${pkg}`, + )) : Widget.Box(), + ), + }) + + const entry = Widget.Entry({ + hexpand: true, + primary_icon_name: icons.ui.search, + on_accept: ({ text }) => { + if (text?.startsWith(":nx")) + nix.run(text.substring(3)) + else if (text?.startsWith(":sh")) + sh.run(text.substring(3)) + else + applauncher.launchFirst() + + App.toggleWindow("launcher") + entry.text = "" + }, + on_change: ({ text }) => { + text ||= "" + favs.reveal_child = text === "" + help.reveal_child = text.split(" ").length === 1 && text?.startsWith(":") + + if (text?.startsWith(":nx")) + nix.filter(text.substring(3)) + else + nix.filter("") + + if (text?.startsWith(":sh")) + sh.filter(text.substring(3)) + else + sh.filter("") + + if (!text?.startsWith(":")) + applauncher.filter(text) + }, + }) + + function focus() { + entry.text = "Search" + entry.set_position(-1) + entry.select_region(0, -1) + entry.grab_focus() + favs.reveal_child = true + } + + const layout = Widget.Box({ + css: width.bind().as(v => `min-width: ${v}pt;`), + class_name: "launcher", + vertical: true, + vpack: "start", + setup: self => self.hook(App, (_, win, visible) => { + if (win !== "launcher") + return + + entry.text = "" + if (visible) + focus() + }), + children: [ + Widget.Box([entry, nixload, shicon]), + favs, + help, + applauncher, + nix, + sh, + ], + }) + + return Widget.Box( + { vertical: true, css: "padding: 1px" }, + Padding("applauncher", { + css: margin.bind().as(v => `min-height: ${v}pt;`), + vexpand: false, + }), + layout, + ) +} + +export default () => PopupWindow({ + name: "launcher", + layout: "top", + child: Launcher(), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/launcher/NixRun.ts b/home/redyf/desktop/addons/ags/config/widget/launcher/NixRun.ts new file mode 100644 index 00000000..cec9e092 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/launcher/NixRun.ts @@ -0,0 +1,118 @@ +import icons from "lib/icons" +import nix, { type Nixpkg } from "service/nix" + +const iconVisible = Variable(false) + +function Item(pkg: Nixpkg) { + const name = Widget.Label({ + class_name: "name", + label: pkg.name.split(".").at(-1), + }) + + const subpkg = pkg.name.includes(".") ? Widget.Label({ + class_name: "description", + hpack: "end", + hexpand: true, + label: ` ${pkg.name.split(".").slice(0, -1).join(".")}`, + }) : null + + const version = Widget.Label({ + class_name: "version", + label: pkg.version, + hexpand: true, + hpack: "end", + }) + + const description = pkg.description ? Widget.Label({ + class_name: "description", + label: pkg.description, + justification: "left", + wrap: true, + hpack: "start", + max_width_chars: 40, + }) : null + + return Widget.Box( + { + attribute: { name: pkg.name }, + vertical: true, + }, + Widget.Separator(), + Widget.Button( + { + class_name: "nix-item", + on_clicked: () => { + nix.run(pkg.name) + App.closeWindow("launcher") + }, + }, + Widget.Box( + { vertical: true }, + Widget.Box([name, version]), + Widget.Box([ + description as ReturnType, + subpkg as ReturnType, + ]), + ), + ), + ) +} + +export function Spinner() { + const icon = Widget.Icon({ + icon: icons.nix.nix, + class_name: "spinner", + css: ` + @keyframes spin { + to { -gtk-icon-transform: rotate(1turn); } + } + + image.spinning { + animation-name: spin; + animation-duration: 1s; + animation-timing-function: linear; + animation-iteration-count: infinite; + } + `, + setup: self => self.hook(nix, () => { + self.toggleClassName("spinning", !nix.ready) + }), + }) + + return Widget.Revealer({ + transition: "slide_left", + child: icon, + reveal_child: Utils.merge([ + nix.bind("ready"), + iconVisible.bind(), + ], (ready, show) => !ready || show), + }) +} + +export function NixRun() { + const list = Widget.Box>({ + vertical: true, + }) + + const revealer = Widget.Revealer({ + child: list, + }) + + async function filter(term: string) { + iconVisible.value = Boolean(term) + + if (!term) + revealer.reveal_child = false + + if (term.trim()) { + const found = await nix.query(term) + list.children = found.map(k => Item(nix.db[k])) + revealer.reveal_child = true + } + } + + return Object.assign(revealer, { + filter, + run: nix.run, + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/launcher/ShRun.ts b/home/redyf/desktop/addons/ags/config/widget/launcher/ShRun.ts new file mode 100644 index 00000000..179e77d3 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/launcher/ShRun.ts @@ -0,0 +1,66 @@ +import icons from "lib/icons" +import sh from "service/sh" + +const iconVisible = Variable(false) + +function Item(bin: string) { + return Widget.Box( + { + attribute: { bin }, + vertical: true, + }, + Widget.Separator(), + Widget.Button({ + child: Widget.Label({ + label: bin, + hpack: "start", + }), + class_name: "sh-item", + on_clicked: () => { + Utils.execAsync(bin) + App.closeWindow("launcher") + }, + }), + ) +} + +export function Icon() { + const icon = Widget.Icon({ + icon: icons.app.terminal, + class_name: "spinner", + }) + + return Widget.Revealer({ + transition: "slide_left", + child: icon, + reveal_child: iconVisible.bind(), + }) +} + +export function ShRun() { + const list = Widget.Box>({ + vertical: true, + }) + + const revealer = Widget.Revealer({ + child: list, + }) + + async function filter(term: string) { + iconVisible.value = Boolean(term) + + if (!term) + revealer.reveal_child = false + + if (term.trim()) { + const found = await sh.query(term) + list.children = found.map(Item) + revealer.reveal_child = true + } + } + + return Object.assign(revealer, { + filter, + run: sh.run, + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/notifications/Notification.ts b/home/redyf/desktop/addons/ags/config/widget/notifications/Notification.ts new file mode 100644 index 00000000..f05fba25 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/notifications/Notification.ts @@ -0,0 +1,138 @@ +import { type Notification } from "types/service/notifications" +import GLib from "gi://GLib" +import icons from "lib/icons" + +const time = (time: number, format = "%H:%M") => GLib.DateTime + .new_from_unix_local(time) + .format(format) + +const NotificationIcon = ({ app_entry, app_icon, image }: Notification) => { + if (image) { + return Widget.Box({ + vpack: "start", + hexpand: false, + class_name: "icon img", + css: ` + background-image: url("${image}"); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + min-width: 78px; + min-height: 78px; + `, + }) + } + + let icon = icons.fallback.notification + if (Utils.lookUpIcon(app_icon)) + icon = app_icon + + if (Utils.lookUpIcon(app_entry || "")) + icon = app_entry || "" + + return Widget.Box({ + vpack: "start", + hexpand: false, + class_name: "icon", + css: ` + min-width: 78px; + min-height: 78px; + `, + child: Widget.Icon({ + icon, + size: 58, + hpack: "center", hexpand: true, + vpack: "center", vexpand: true, + }), + }) +} + +export default (notification: Notification) => { + const content = Widget.Box({ + class_name: "content", + children: [ + NotificationIcon(notification), + Widget.Box({ + hexpand: true, + vertical: true, + children: [ + Widget.Box({ + children: [ + Widget.Label({ + class_name: "title", + xalign: 0, + justification: "left", + hexpand: true, + max_width_chars: 24, + truncate: "end", + wrap: true, + label: notification.summary.trim(), + use_markup: true, + }), + Widget.Label({ + class_name: "time", + vpack: "start", + label: time(notification.time), + }), + Widget.Button({ + class_name: "close-button", + vpack: "start", + child: Widget.Icon("window-close-symbolic"), + on_clicked: notification.close, + }), + ], + }), + Widget.Label({ + class_name: "description", + hexpand: true, + use_markup: true, + xalign: 0, + justification: "left", + label: notification.body.trim(), + max_width_chars: 24, + wrap: true, + }), + ], + }), + ], + }) + + const actionsbox = notification.actions.length > 0 ? Widget.Revealer({ + transition: "slide_down", + child: Widget.EventBox({ + child: Widget.Box({ + class_name: "actions horizontal", + children: notification.actions.map(action => Widget.Button({ + class_name: "action-button", + on_clicked: () => notification.invoke(action.id), + hexpand: true, + child: Widget.Label(action.label), + })), + }), + }), + }) : null + + const eventbox = Widget.EventBox({ + vexpand: false, + on_primary_click: notification.dismiss, + on_hover() { + if (actionsbox) + actionsbox.reveal_child = true + }, + on_hover_lost() { + if (actionsbox) + actionsbox.reveal_child = true + + notification.dismiss() + }, + child: Widget.Box({ + vertical: true, + children: actionsbox ? [content, actionsbox] : [content], + }), + }) + + return Widget.Box({ + class_name: `notification ${notification.urgency}`, + child: eventbox, + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/notifications/NotificationPopups.ts b/home/redyf/desktop/addons/ags/config/widget/notifications/NotificationPopups.ts new file mode 100644 index 00000000..a4a2b540 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/notifications/NotificationPopups.ts @@ -0,0 +1,90 @@ +import Notification from "./Notification" +import options from "options" + +const notifications = await Service.import("notifications") +const { transition } = options +const { position } = options.notifications +const { timeout, idle } = Utils + +function Animated(id: number) { + const n = notifications.getNotification(id)! + const widget = Notification(n) + + const inner = Widget.Revealer({ + transition: "slide_left", + transition_duration: transition.value, + child: widget, + }) + + const outer = Widget.Revealer({ + transition: "slide_down", + transition_duration: transition.value, + child: inner, + }) + + const box = Widget.Box({ + hpack: "end", + child: outer, + }) + + idle(() => { + outer.reveal_child = true + timeout(transition.value, () => { + inner.reveal_child = true + }) + }) + + return Object.assign(box, { + dismiss() { + inner.reveal_child = false + timeout(transition.value, () => { + outer.reveal_child = false + timeout(transition.value, () => { + box.destroy() + }) + }) + }, + }) +} + +function PopupList() { + const map: Map> = new Map + const box = Widget.Box({ + hpack: "end", + vertical: true, + css: options.notifications.width.bind().as(w => `min-width: ${w}px;`), + }) + + function remove(_: unknown, id: number) { + map.get(id)?.dismiss() + map.delete(id) + } + + return box + .hook(notifications, (_, id: number) => { + if (id !== undefined) { + if (map.has(id)) + remove(null, id) + + if (notifications.dnd) + return + + const w = Animated(id) + map.set(id, w) + box.children = [w, ...box.children] + } + }, "notified") + .hook(notifications, remove, "dismissed") + .hook(notifications, remove, "closed") +} + +export default (monitor: number) => Widget.Window({ + monitor, + name: `notifications${monitor}`, + anchor: position.bind(), + class_name: "notifications", + child: Widget.Box({ + css: "padding: 2px;", + child: PopupList(), + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/osd/OSD.ts b/home/redyf/desktop/addons/ags/config/widget/osd/OSD.ts new file mode 100644 index 00000000..467e6dc9 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/osd/OSD.ts @@ -0,0 +1,111 @@ +import { icon } from "lib/utils" +import icons from "lib/icons" +import Progress from "./Progress" +import brightness from "service/brightness" +import options from "options" + +const audio = await Service.import("audio") +const { progress, microphone } = options.osd + +const DELAY = 2500 + +function OnScreenProgress(vertical: boolean) { + const indicator = Widget.Icon({ + size: 42, + vpack: "start", + }) + const progress = Progress({ + vertical, + width: vertical ? 42 : 300, + height: vertical ? 300 : 42, + child: indicator, + }) + + const revealer = Widget.Revealer({ + transition: "slide_left", + child: progress, + }) + + let count = 0 + function show(value: number, icon: string) { + revealer.reveal_child = true + indicator.icon = icon + progress.setValue(value) + count++ + Utils.timeout(DELAY, () => { + count-- + + if (count === 0) + revealer.reveal_child = false + }) + } + + return revealer + .hook(brightness, () => show( + brightness.screen, + icons.brightness.screen, + ), "notify::screen") + .hook(brightness, () => show( + brightness.kbd, + icons.brightness.keyboard, + ), "notify::kbd") + .hook(audio.speaker, () => show( + audio.speaker.volume, + icon(audio.speaker.icon_name || "", icons.audio.type.speaker), + ), "notify::volume") +} + +function MicrophoneMute() { + const icon = Widget.Icon({ + class_name: "microphone", + }) + + const revealer = Widget.Revealer({ + transition: "slide_up", + child: icon, + }) + + let count = 0 + let mute = audio.microphone.stream?.is_muted ?? false + + return revealer.hook(audio.microphone, () => Utils.idle(() => { + if (mute !== audio.microphone.stream?.is_muted) { + mute = audio.microphone.stream!.is_muted + icon.icon = icons.audio.mic[mute ? "muted" : "high"] + revealer.reveal_child = true + count++ + + Utils.timeout(DELAY, () => { + count-- + if (count === 0) + revealer.reveal_child = false + }) + } + })) +} + +export default (monitor: number) => Widget.Window({ + monitor, + name: `indicator${monitor}`, + class_name: "indicator", + layer: "overlay", + click_through: true, + anchor: ["right", "left", "top", "bottom"], + child: Widget.Box({ + css: "padding: 2px;", + expand: true, + child: Widget.Overlay( + { child: Widget.Box({ expand: true }) }, + Widget.Box({ + hpack: progress.pack.h.bind(), + vpack: progress.pack.v.bind(), + child: progress.vertical.bind().as(OnScreenProgress), + }), + Widget.Box({ + hpack: microphone.pack.h.bind(), + vpack: microphone.pack.v.bind(), + child: MicrophoneMute(), + }), + ), + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/osd/Progress.ts b/home/redyf/desktop/addons/ags/config/widget/osd/Progress.ts new file mode 100644 index 00000000..bcf27da3 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/osd/Progress.ts @@ -0,0 +1,74 @@ +import type Gtk from "gi://Gtk?version=3.0" +import GLib from "gi://GLib?version=2.0" +import { range } from "lib/utils" +import options from "options" + +type ProgressProps = { + height?: number + width?: number + vertical?: boolean + child: Gtk.Widget +} + +export default ({ + height = 18, + width = 180, + vertical = false, + child, +}: ProgressProps) => { + const fill = Widget.Box({ + class_name: "fill", + hexpand: vertical, + vexpand: !vertical, + hpack: vertical ? "fill" : "start", + vpack: vertical ? "end" : "fill", + child, + }) + + const container = Widget.Box({ + class_name: "progress", + child: fill, + css: ` + min-width: ${width}px; + min-height: ${height}px; + `, + }) + + let fill_size = 0 + let animations: number[] = [] + + return Object.assign(container, { + setValue(value: number) { + if (value < 0) + return + + if (animations.length > 0) { + for (const id of animations) + GLib.source_remove(id) + + animations = [] + } + + const axis = vertical ? "height" : "width" + const axisv = vertical ? height : width + const min = vertical ? width : height + const preferred = (axisv - min) * value + min + + if (!fill_size) { + fill_size = preferred + fill.css = `min-${axis}: ${preferred}px;` + return + } + + const frames = options.transition.value / 10 + const goal = preferred - fill_size + const step = goal / frames + + animations = range(frames, 0).map(i => Utils.timeout(5 * i, () => { + fill_size += step + fill.css = `min-${axis}: ${fill_size}px` + animations.shift() + })) + }, + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/overview/Overview.ts b/home/redyf/desktop/addons/ags/config/widget/overview/Overview.ts new file mode 100644 index 00000000..8911920d --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/overview/Overview.ts @@ -0,0 +1,41 @@ +import PopupWindow from "widget/PopupWindow" +import Workspace from "./Workspace" +import options from "options" +import { range } from "lib/utils" + +const hyprland = await Service.import("hyprland") + +const Overview = (ws: number) => Widget.Box({ + class_name: "overview horizontal", + children: ws > 0 + ? range(ws).map(Workspace) + : hyprland.workspaces + .map(({ id }) => Workspace(id)) + .sort((a, b) => a.attribute.id - b.attribute.id), + + setup: w => { + if (ws > 0) + return + + w.hook(hyprland, (w, id?: string) => { + if (id === undefined) + return + + w.children = w.children + .filter(ch => ch.attribute.id !== Number(id)) + }, "workspace-removed") + w.hook(hyprland, (w, id?: string) => { + if (id === undefined) + return + + w.children = [...w.children, Workspace(Number(id))] + .sort((a, b) => a.attribute.id - b.attribute.id) + }, "workspace-added") + }, +}) + +export default () => PopupWindow({ + name: "overview", + layout: "center", + child: options.overview.workspaces.bind().as(Overview), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/overview/Window.ts b/home/redyf/desktop/addons/ags/config/widget/overview/Window.ts new file mode 100644 index 00000000..02f71eb2 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/overview/Window.ts @@ -0,0 +1,48 @@ +import { type Client } from "types/service/hyprland" +import { createSurfaceFromWidget, icon } from "lib/utils" +import Gdk from "gi://Gdk" +import Gtk from "gi://Gtk?version=3.0" +import options from "options" +import icons from "lib/icons" + +const monochrome = options.overview.monochromeIcon +const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)] +const hyprland = await Service.import("hyprland") +const apps = await Service.import("applications") +const dispatch = (args: string) => hyprland.messageAsync(`dispatch ${args}`) + +export default ({ address, size: [w, h], class: c, title }: Client) => Widget.Button({ + class_name: "client", + attribute: { address }, + tooltip_text: `${title}`, + child: Widget.Icon({ + css: options.overview.scale.bind().as(v => ` + min-width: ${(v / 100) * w}px; + min-height: ${(v / 100) * h}px; + `), + icon: monochrome.bind().as(m => { + const app = apps.list.find(app => app.match(c)) + if (!app) + return icons.fallback.executable + (m ? "-symbolic" : "") + + + return icon( + app.icon_name + (m ? "-symbolic" : ""), + icons.fallback.executable + (m ? "-symbolic" : ""), + ) + }), + }), + on_secondary_click: () => dispatch(`closewindow address:${address}`), + on_clicked: () => { + dispatch(`focuswindow address:${address}`) + App.closeWindow("overview") + }, + setup: btn => btn + .on("drag-data-get", (_w, _c, data) => data.set_text(address, address.length)) + .on("drag-begin", (_, context) => { + Gtk.drag_set_icon_surface(context, createSurfaceFromWidget(btn)) + btn.toggleClassName("hidden", true) + }) + .on("drag-end", () => btn.toggleClassName("hidden", false)) + .drag_source_set(Gdk.ModifierType.BUTTON1_MASK, TARGET, Gdk.DragAction.COPY), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/overview/Workspace.ts b/home/redyf/desktop/addons/ags/config/widget/overview/Workspace.ts new file mode 100644 index 00000000..1b8d60b3 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/overview/Workspace.ts @@ -0,0 +1,76 @@ +import Window from "./Window" +import Gdk from "gi://Gdk" +import Gtk from "gi://Gtk?version=3.0" +import options from "options" + +const TARGET = [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)] +const scale = (size: number) => (options.overview.scale.value / 100) * size +const hyprland = await Service.import("hyprland") + +const dispatch = (args: string) => hyprland.messageAsync(`dispatch ${args}`) + +const size = (id: number) => { + const def = { h: 1080, w: 1920 } + const ws = hyprland.getWorkspace(id) + if (!ws) + return def + + const mon = hyprland.getMonitor(ws.monitorID) + return mon ? { h: mon.height, w: mon.width } : def +} + +export default (id: number) => { + const fixed = Widget.Fixed() + + // TODO: early return if position is unchaged + async function update() { + const json = await hyprland.messageAsync("j/clients").catch(() => null) + if (!json) + return + + fixed.get_children().forEach(ch => ch.destroy()) + const clients = JSON.parse(json) as typeof hyprland.clients + clients + .filter(({ workspace }) => workspace.id === id) + .forEach(c => { + const x = c.at[0] - (hyprland.getMonitor(c.monitor)?.x || 0) + const y = c.at[1] - (hyprland.getMonitor(c.monitor)?.y || 0) + c.mapped && fixed.put(Window(c), scale(x), scale(y)) + }) + fixed.show_all() + } + + return Widget.Box({ + attribute: { id }, + tooltipText: `${id}`, + class_name: "workspace", + vpack: "center", + css: options.overview.scale.bind().as(v => ` + min-width: ${(v / 100) * size(id).w}px; + min-height: ${(v / 100) * size(id).h}px; + `), + setup(box) { + box.hook(options.overview.scale, update) + box.hook(hyprland, update, "notify::clients") + box.hook(hyprland.active.client, update) + box.hook(hyprland.active.workspace, () => { + box.toggleClassName("active", hyprland.active.workspace.id === id) + }) + }, + child: Widget.EventBox({ + expand: true, + on_primary_click: () => { + App.closeWindow("overview") + dispatch(`workspace ${id}`) + }, + setup: eventbox => { + eventbox.drag_dest_set(Gtk.DestDefaults.ALL, TARGET, Gdk.DragAction.COPY) + eventbox.connect("drag-data-received", (_w, _c, _x, _y, data) => { + const address = new TextDecoder().decode(data.get_data()) + dispatch(`movetoworkspacesilent ${id},address:${address}`) + }) + }, + child: fixed, + }), + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/powermenu/PowerMenu.ts b/home/redyf/desktop/addons/ags/config/widget/powermenu/PowerMenu.ts new file mode 100644 index 00000000..fe0a0e97 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/powermenu/PowerMenu.ts @@ -0,0 +1,56 @@ +import PopupWindow from "widget/PopupWindow" +import powermenu, { type Action } from "service/powermenu" +import icons from "lib/icons" +import options from "options" +import type Gtk from "gi://Gtk?version=3.0" + +const { layout, labels } = options.powermenu + +const SysButton = (action: Action, label: string) => Widget.Button({ + on_clicked: () => powermenu.action(action), + child: Widget.Box({ + vertical: true, + class_name: "system-button", + children: [ + Widget.Icon(icons.powermenu[action]), + Widget.Label({ + label, + visible: labels.bind(), + }), + ], + }), +}) + +export default () => PopupWindow({ + name: "powermenu", + transition: "crossfade", + child: Widget.Box({ + class_name: "powermenu horizontal", + setup: self => self.hook(layout, () => { + self.toggleClassName("box", layout.value === "box") + self.toggleClassName("line", layout.value === "line") + }), + children: layout.bind().as(layout => { + switch (layout) { + case "line": return [ + SysButton("shutdown", "Shutdown"), + SysButton("logout", "Log Out"), + SysButton("reboot", "Reboot"), + SysButton("sleep", "Sleep"), + ] + case "box": return [ + Widget.Box( + { vertical: true }, + SysButton("shutdown", "Shutdown"), + SysButton("logout", "Log Out"), + ), + Widget.Box( + { vertical: true }, + SysButton("reboot", "Reboot"), + SysButton("sleep", "Sleep"), + ), + ] + } + }), + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/powermenu/Verification.ts b/home/redyf/desktop/addons/ags/config/widget/powermenu/Verification.ts new file mode 100644 index 00000000..3145ce59 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/powermenu/Verification.ts @@ -0,0 +1,47 @@ +import PopupWindow from "widget/PopupWindow" +import powermenu from "service/powermenu" + +export default () => PopupWindow({ + name: "verification", + transition: "crossfade", + child: Widget.Box({ + class_name: "verification", + vertical: true, + children: [ + Widget.Box({ + class_name: "text-box", + vertical: true, + children: [ + Widget.Label({ + class_name: "title", + label: powermenu.bind("title"), + }), + Widget.Label({ + class_name: "desc", + label: "Are you sure?", + }), + ], + }), + Widget.Box({ + class_name: "buttons horizontal", + vexpand: true, + vpack: "end", + homogeneous: true, + children: [ + Widget.Button({ + child: Widget.Label("No"), + on_clicked: () => App.toggleWindow("verification"), + setup: self => self.hook(App, (_, name: string, visible: boolean) => { + if (name === "verification" && visible) + self.grab_focus() + }), + }), + Widget.Button({ + child: Widget.Label("Yes"), + on_clicked: powermenu.exec, + }), + ], + }), + ], + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/QuickSettings.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/QuickSettings.ts new file mode 100644 index 00000000..4bc46bc9 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/QuickSettings.ts @@ -0,0 +1,84 @@ +import type Gtk from "gi://Gtk?version=3.0" +import { ProfileSelector, ProfileToggle } from "./widgets/PowerProfile" +import { Header } from "./widgets/Header" +import { Volume, Microphone, SinkSelector, AppMixer } from "./widgets/Volume" +import { Brightness } from "./widgets/Brightness" +import { NetworkToggle, WifiSelection } from "./widgets/Network" +import { BluetoothToggle, BluetoothDevices } from "./widgets/Bluetooth" +import { DND } from "./widgets/DND" +import { DarkModeToggle } from "./widgets/DarkMode" +import { MicMute } from "./widgets/MicMute" +import { Media } from "./widgets/Media" +import PopupWindow from "widget/PopupWindow" +import options from "options" + +const { bar, quicksettings } = options +const media = (await Service.import("mpris")).bind("players") +const layout = Utils.derive([bar.position, quicksettings.position], (bar, qs) => + `${bar}-${qs}` as const, +) + +const Row = ( + toggles: Array<() => Gtk.Widget> = [], + menus: Array<() => Gtk.Widget> = [], +) => Widget.Box({ + vertical: true, + children: [ + Widget.Box({ + homogeneous: true, + class_name: "row horizontal", + children: toggles.map(w => w()), + }), + ...menus.map(w => w()), + ], +}) + +const Settings = () => Widget.Box({ + vertical: true, + class_name: "quicksettings vertical", + css: quicksettings.width.bind().as(w => `min-width: ${w}px;`), + children: [ + Header(), + Widget.Box({ + class_name: "sliders-box vertical", + vertical: true, + children: [ + Row( + [Volume], + [SinkSelector, AppMixer], + ), + Microphone(), + Brightness(), + ], + }), + Row( + [NetworkToggle, BluetoothToggle], + [WifiSelection, BluetoothDevices], + ), + Row( + [ProfileToggle, DarkModeToggle], + [ProfileSelector], + ), + Row([MicMute, DND]), + Widget.Box({ + visible: media.as(l => l.length > 0), + child: Media(), + }), + ], +}) + +const QuickSettings = () => PopupWindow({ + name: "quicksettings", + exclusivity: "exclusive", + transition: bar.position.bind().as(pos => pos === "top" ? "slide_down" : "slide_up"), + layout: layout.value, + child: Settings(), +}) + +export function setupQuickSettings() { + App.addWindow(QuickSettings()) + layout.connect("changed", () => { + App.removeWindow("quicksettings") + App.addWindow(QuickSettings()) + }) +} diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/ToggleButton.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/ToggleButton.ts new file mode 100644 index 00000000..1380acf3 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/ToggleButton.ts @@ -0,0 +1,154 @@ +import { type IconProps } from "types/widgets/icon" +import { type LabelProps } from "types/widgets/label" +import type GObject from "gi://GObject?version=2.0" +import type Gtk from "gi://Gtk?version=3.0" +import icons from "lib/icons" + +export const opened = Variable("") +App.connect("window-toggled", (_, name: string, visible: boolean) => { + if (name === "quicksettings" && !visible) + Utils.timeout(500, () => opened.value = "") +}) + +export const Arrow = (name: string, activate?: false | (() => void)) => { + let deg = 0 + let iconOpened = false + const icon = Widget.Icon(icons.ui.arrow.right).hook(opened, () => { + if (opened.value === name && !iconOpened || opened.value !== name && iconOpened) { + const step = opened.value === name ? 10 : -10 + iconOpened = !iconOpened + for (let i = 0; i < 9; ++i) { + Utils.timeout(15 * i, () => { + deg += step + icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`) + }) + } + } + }) + return Widget.Button({ + child: icon, + class_name: "arrow", + on_clicked: () => { + opened.value = opened.value === name ? "" : name + if (typeof activate === "function") + activate() + }, + }) +} + +type ArrowToggleButtonProps = { + name: string + icon: IconProps["icon"] + label: LabelProps["label"] + activate: () => void + deactivate: () => void + activateOnArrow?: boolean + connection: [GObject.Object, () => boolean] +} +export const ArrowToggleButton = ({ + name, + icon, + label, + activate, + deactivate, + activateOnArrow = true, + connection: [service, condition], +}: ArrowToggleButtonProps) => Widget.Box({ + class_name: "toggle-button", + setup: self => self.hook(service, () => { + self.toggleClassName("active", condition()) + }), + children: [ + Widget.Button({ + child: Widget.Box({ + hexpand: true, + children: [ + Widget.Icon({ + class_name: "icon", + icon, + }), + Widget.Label({ + class_name: "label", + max_width_chars: 10, + truncate: "end", + label, + }), + ], + }), + on_clicked: () => { + if (condition()) { + deactivate() + if (opened.value === name) + opened.value = "" + } else { + activate() + } + }, + }), + Arrow(name, activateOnArrow && activate), + ], +}) + +type MenuProps = { + name: string + icon: IconProps["icon"] + title: LabelProps["label"] + content: Gtk.Widget[] +} +export const Menu = ({ name, icon, title, content }: MenuProps) => Widget.Revealer({ + transition: "slide_down", + reveal_child: opened.bind().as(v => v === name), + child: Widget.Box({ + class_names: ["menu", name], + vertical: true, + children: [ + Widget.Box({ + class_name: "title-box", + children: [ + Widget.Icon({ + class_name: "icon", + icon, + }), + Widget.Label({ + class_name: "title", + truncate: "end", + label: title, + }), + ], + }), + Widget.Separator(), + Widget.Box({ + vertical: true, + class_name: "content vertical", + children: content, + }), + ], + }), +}) + +type SimpleToggleButtonProps = { + icon: IconProps["icon"] + label: LabelProps["label"] + toggle: () => void + connection: [GObject.Object, () => boolean] +} +export const SimpleToggleButton = ({ + icon, + label, + toggle, + connection: [service, condition], +}: SimpleToggleButtonProps) => Widget.Button({ + on_clicked: toggle, + class_name: "simple-toggle", + setup: self => self.hook(service, () => { + self.toggleClassName("active", condition()) + }), + child: Widget.Box([ + Widget.Icon({ icon }), + Widget.Label({ + max_width_chars: 10, + truncate: "end", + label, + }), + ]), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Bluetooth.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Bluetooth.ts new file mode 100644 index 00000000..649e6549 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Bluetooth.ts @@ -0,0 +1,61 @@ +import { type BluetoothDevice } from "types/service/bluetooth" +import { Menu, ArrowToggleButton } from "../ToggleButton" +import icons from "lib/icons" + +const bluetooth = await Service.import("bluetooth") + +export const BluetoothToggle = () => ArrowToggleButton({ + name: "bluetooth", + icon: bluetooth.bind("enabled").as(p => icons.bluetooth[p ? "enabled" : "disabled"]), + label: Utils.watch("Disabled", bluetooth, () => { + if (!bluetooth.enabled) + return "Disabled" + + if (bluetooth.connected_devices.length === 1) + return bluetooth.connected_devices[0].alias + + return `${bluetooth.connected_devices.length} Connected` + }), + connection: [bluetooth, () => bluetooth.enabled], + deactivate: () => bluetooth.enabled = false, + activate: () => bluetooth.enabled = true, +}) + +const DeviceItem = (device: BluetoothDevice) => Widget.Box({ + children: [ + Widget.Icon(device.icon_name + "-symbolic"), + Widget.Label(device.name), + Widget.Label({ + label: `${device.battery_percentage}%`, + visible: device.bind("battery_percentage").as(p => p > 0), + }), + Widget.Box({ hexpand: true }), + Widget.Spinner({ + active: device.bind("connecting"), + visible: device.bind("connecting"), + }), + Widget.Switch({ + active: device.connected, + visible: device.bind("connecting").as(p => !p), + setup: self => self.on("notify::active", () => { + device.setConnection(self.active) + }), + }), + ], +}) + +export const BluetoothDevices = () => Menu({ + name: "bluetooth", + icon: icons.bluetooth.disabled, + title: "Bluetooth", + content: [ + Widget.Box({ + class_name: "bluetooth-devices", + hexpand: true, + vertical: true, + children: bluetooth.bind("devices").as(ds => ds + .filter(d => d.name) + .map(DeviceItem)), + }), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Brightness.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Brightness.ts new file mode 100644 index 00000000..a3ce5657 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Brightness.ts @@ -0,0 +1,23 @@ +import icons from "lib/icons" +import brightness from "service/brightness" + +const BrightnessSlider = () => Widget.Slider({ + draw_value: false, + hexpand: true, + value: brightness.bind("screen"), + on_change: ({ value }) => brightness.screen = value, +}) + +export const Brightness = () => Widget.Box({ + class_name: "brightness", + children: [ + Widget.Button({ + vpack: "center", + child: Widget.Icon(icons.brightness.indicator), + on_clicked: () => brightness.screen = 0, + tooltip_text: brightness.bind("screen").as(v => + `Screen Brightness: ${Math.floor(v * 100)}%`), + }), + BrightnessSlider(), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/DND.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/DND.ts new file mode 100644 index 00000000..7fc1fd03 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/DND.ts @@ -0,0 +1,12 @@ +import { SimpleToggleButton } from "../ToggleButton" +import icons from "lib/icons" + +const n = await Service.import("notifications") +const dnd = n.bind("dnd") + +export const DND = () => SimpleToggleButton({ + icon: dnd.as(dnd => icons.notifications[dnd ? "silent" : "noisy"]), + label: dnd.as(dnd => dnd ? "Silent" : "Noisy"), + toggle: () => n.dnd = !n.dnd, + connection: [n, () => n.dnd], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/DarkMode.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/DarkMode.ts new file mode 100644 index 00000000..9ec94df1 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/DarkMode.ts @@ -0,0 +1,12 @@ +import { SimpleToggleButton } from "../ToggleButton" +import icons from "lib/icons" +import options from "options" + +const { scheme } = options.theme + +export const DarkModeToggle = () => SimpleToggleButton({ + icon: scheme.bind().as(s => icons.color[s]), + label: scheme.bind().as(s => s === "dark" ? "Dark" : "Light"), + toggle: () => scheme.value = scheme.value === "dark" ? "light" : "dark", + connection: [scheme, () => scheme.value === "dark"], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Header.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Header.ts new file mode 100644 index 00000000..72bada0b --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Header.ts @@ -0,0 +1,63 @@ +import icons from "lib/icons" +import { uptime } from "lib/variables" +import options from "options" +import powermenu, { Action } from "service/powermenu" + +const battery = await Service.import("battery") +const { image, size } = options.quicksettings.avatar + +function up(up: number) { + const h = Math.floor(up / 60) + const m = Math.floor(up % 60) + return `${h}h ${m < 10 ? "0" + m : m}m` +} + +const Avatar = () => Widget.Box({ + class_name: "avatar", + css: Utils.merge([image.bind(), size.bind()], (img, size) => ` + min-width: ${size}px; + min-height: ${size}px; + background-image: url('${img}'); + background-size: cover; + `), +}) + +const SysButton = (action: Action) => Widget.Button({ + vpack: "center", + child: Widget.Icon(icons.powermenu[action]), + on_clicked: () => powermenu.action(action), +}) + +export const Header = () => Widget.Box( + { class_name: "header horizontal" }, + Avatar(), + Widget.Box({ + vertical: true, + vpack: "center", + children: [ + Widget.Box({ + visible: battery.bind("available"), + children: [ + Widget.Icon({ icon: battery.bind("icon_name") }), + Widget.Label({ label: battery.bind("percent").as(p => `${p}%`) }), + ], + }), + Widget.Box([ + Widget.Icon({ icon: icons.ui.time }), + Widget.Label({ label: uptime.bind().as(up) }), + ]), + ], + }), + Widget.Box({ hexpand: true }), + Widget.Button({ + vpack: "center", + child: Widget.Icon(icons.ui.settings), + on_clicked: () => { + App.closeWindow("quicksettings") + App.closeWindow("settings-dialog") + App.openWindow("settings-dialog") + }, + }), + SysButton("logout"), + SysButton("shutdown"), +) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Media.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Media.ts new file mode 100644 index 00000000..52254ea6 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Media.ts @@ -0,0 +1,153 @@ +import { type MprisPlayer } from "types/service/mpris" +import icons from "lib/icons" +import options from "options" +import { icon } from "lib/utils" + +const mpris = await Service.import("mpris") +const players = mpris.bind("players") +const { media } = options.quicksettings + +function lengthStr(length: number) { + const min = Math.floor(length / 60) + const sec = Math.floor(length % 60) + const sec0 = sec < 10 ? "0" : "" + return `${min}:${sec0}${sec}` +} + +const Player = (player: MprisPlayer) => { + const cover = Widget.Box({ + class_name: "cover", + vpack: "start", + css: Utils.merge([ + player.bind("cover_path"), + player.bind("track_cover_url"), + media.coverSize.bind(), + ], (path, url, size) => ` + min-width: ${size}px; + min-height: ${size}px; + background-image: url('${path || url}'); + `), + }) + + const title = Widget.Label({ + class_name: "title", + max_width_chars: 20, + truncate: "end", + hpack: "start", + label: player.bind("track_title"), + }) + + const artist = Widget.Label({ + class_name: "artist", + max_width_chars: 20, + truncate: "end", + hpack: "start", + label: player.bind("track_artists").as(a => a.join(", ")), + }) + + const positionSlider = Widget.Slider({ + class_name: "position", + draw_value: false, + on_change: ({ value }) => player.position = value * player.length, + setup: self => { + const update = () => { + const { length, position } = player + self.visible = length > 0 + self.value = length > 0 ? position / length : 0 + } + self.hook(player, update) + self.hook(player, update, "position") + self.poll(1000, update) + }, + }) + + const positionLabel = Widget.Label({ + class_name: "position", + hpack: "start", + setup: self => { + const update = (_: unknown, time?: number) => { + self.label = lengthStr(time || player.position) + self.visible = player.length > 0 + } + self.hook(player, update, "position") + self.poll(1000, update) + }, + }) + + const lengthLabel = Widget.Label({ + class_name: "length", + hpack: "end", + visible: player.bind("length").as(l => l > 0), + label: player.bind("length").as(lengthStr), + }) + + const playericon = Widget.Icon({ + class_name: "icon", + hexpand: true, + hpack: "end", + vpack: "start", + tooltip_text: player.identity || "", + icon: Utils.merge([player.bind("entry"), media.monochromeIcon.bind()], (e, s) => { + const name = `${e}${s ? "-symbolic" : ""}` + return icon(name, icons.fallback.audio) + }), + }) + + const playPause = Widget.Button({ + class_name: "play-pause", + on_clicked: () => player.playPause(), + visible: player.bind("can_play"), + child: Widget.Icon({ + icon: player.bind("play_back_status").as(s => { + switch (s) { + case "Playing": return icons.mpris.playing + case "Paused": + case "Stopped": return icons.mpris.stopped + } + }), + }), + }) + + const prev = Widget.Button({ + on_clicked: () => player.previous(), + visible: player.bind("can_go_prev"), + child: Widget.Icon(icons.mpris.prev), + }) + + const next = Widget.Button({ + on_clicked: () => player.next(), + visible: player.bind("can_go_next"), + child: Widget.Icon(icons.mpris.next), + }) + + return Widget.Box( + { class_name: "player", vexpand: false }, + cover, + Widget.Box( + { vertical: true }, + Widget.Box([ + title, + playericon, + ]), + artist, + Widget.Box({ vexpand: true }), + positionSlider, + Widget.CenterBox({ + class_name: "footer horizontal", + start_widget: positionLabel, + center_widget: Widget.Box([ + prev, + playPause, + next, + ]), + end_widget: lengthLabel, + }), + ), + ) +} + +export const Media = () => Widget.Box({ + vertical: true, + class_name: "media vertical", + children: players.as(p => p.map(Player)), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/MicMute.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/MicMute.ts new file mode 100644 index 00000000..b6e9454e --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/MicMute.ts @@ -0,0 +1,18 @@ +import { SimpleToggleButton } from "../ToggleButton" +import icons from "lib/icons" +const { microphone } = await Service.import("audio") + +const icon = () => microphone.is_muted || microphone.stream?.is_muted + ? icons.audio.mic.muted + : icons.audio.mic.high + +const label = () => microphone.is_muted || microphone.stream?.is_muted + ? "Muted" + : "Unmuted" + +export const MicMute = () => SimpleToggleButton({ + icon: Utils.watch(icon(), microphone, icon), + label: Utils.watch(label(), microphone, label), + toggle: () => microphone.is_muted = !microphone.is_muted, + connection: [microphone, () => microphone?.is_muted || false], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Network.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Network.ts new file mode 100644 index 00000000..21f7a0ed --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Network.ts @@ -0,0 +1,64 @@ +import { Menu, ArrowToggleButton } from "../ToggleButton" +import icons from "lib/icons.js" +import { dependencies, sh } from "lib/utils" +import options from "options" +const { wifi } = await Service.import("network") + +export const NetworkToggle = () => ArrowToggleButton({ + name: "network", + icon: wifi.bind("icon_name"), + label: wifi.bind("ssid").as(ssid => ssid || "Not Connected"), + connection: [wifi, () => wifi.enabled], + deactivate: () => wifi.enabled = false, + activate: () => { + wifi.enabled = true + wifi.scan() + }, +}) + +export const WifiSelection = () => Menu({ + name: "network", + icon: wifi.bind("icon_name"), + title: "Wifi Selection", + content: [ + Widget.Box({ + vertical: true, + setup: self => self.hook(wifi, () => self.children = + wifi.access_points + .sort((a, b) => b.strength - a.strength) + .slice(0, 10) + .map(ap => Widget.Button({ + on_clicked: () => { + if (dependencies("nmcli")) + Utils.execAsync(`nmcli device wifi connect ${ap.bssid}`) + }, + child: Widget.Box({ + children: [ + Widget.Icon(ap.iconName), + Widget.Label(ap.ssid || ""), + Widget.Icon({ + icon: icons.ui.tick, + hexpand: true, + hpack: "end", + setup: self => Utils.idle(() => { + if (!self.is_destroyed) + self.visible = ap.active + }), + }), + ], + }), + })), + ), + }), + Widget.Separator(), + Widget.Button({ + on_clicked: () => sh(options.quicksettings.networkSettings.value), + child: Widget.Box({ + children: [ + Widget.Icon(icons.ui.settings), + Widget.Label("Network"), + ], + }), + }), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/PowerProfile.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/PowerProfile.ts new file mode 100644 index 00000000..f566aaf6 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/PowerProfile.ts @@ -0,0 +1,99 @@ +import { ArrowToggleButton, Menu } from "../ToggleButton" +import icons from "lib/icons" + +import asusctl from "service/asusctl" +const asusprof = asusctl.bind("profile") + +const AsusProfileToggle = () => ArrowToggleButton({ + name: "asusctl-profile", + icon: asusprof.as(p => icons.asusctl.profile[p]), + label: asusprof, + connection: [asusctl, () => asusctl.profile !== "Balanced"], + activate: () => asusctl.setProfile("Quiet"), + deactivate: () => asusctl.setProfile("Balanced"), + activateOnArrow: false, +}) + +const AsusProfileSelector = () => Menu({ + name: "asusctl-profile", + icon: asusprof.as(p => icons.asusctl.profile[p]), + title: "Profile Selector", + content: [ + Widget.Box({ + vertical: true, + hexpand: true, + children: [ + Widget.Box({ + vertical: true, + children: asusctl.profiles.map(prof => Widget.Button({ + on_clicked: () => asusctl.setProfile(prof), + child: Widget.Box({ + children: [ + Widget.Icon(icons.asusctl.profile[prof]), + Widget.Label(prof), + ], + }), + })), + }), + ], + }), + Widget.Separator(), + Widget.Button({ + on_clicked: () => Utils.execAsync("rog-control-center"), + child: Widget.Box({ + children: [ + Widget.Icon(icons.ui.settings), + Widget.Label("Rog Control Center"), + ], + }), + }), + ], +}) + + +const pp = await Service.import("powerprofiles") +const profile = pp.bind("active_profile") +const profiles = pp.profiles.map(p => p.Profile) + +const pretty = (str: string) => str + .split("-") + .map(str => `${str.at(0)?.toUpperCase()}${str.slice(1)}`) + .join(" ") + +const PowerProfileToggle = () => ArrowToggleButton({ + name: "asusctl-profile", + icon: profile.as(p => icons.powerprofile[p]), + label: profile.as(pretty), + connection: [pp, () => pp.active_profile !== profiles[1]], + activate: () => pp.active_profile = profiles[0], + deactivate: () => pp.active_profile = profiles[1], + activateOnArrow: false, +}) + +const PowerProfileSelector = () => Menu({ + name: "asusctl-profile", + icon: profile.as(p => icons.powerprofile[p]), + title: "Profile Selector", + content: [Widget.Box({ + vertical: true, + hexpand: true, + child: Widget.Box({ + vertical: true, + children: profiles.map(prof => Widget.Button({ + on_clicked: () => pp.active_profile = prof, + child: Widget.Box({ + children: [ + Widget.Icon(icons.powerprofile[prof]), + Widget.Label(pretty(prof)), + ], + }), + })), + }), + })], +}) + +export const ProfileToggle = asusctl.available + ? AsusProfileToggle : PowerProfileToggle + +export const ProfileSelector = asusctl.available + ? AsusProfileSelector : PowerProfileSelector diff --git a/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Volume.ts b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Volume.ts new file mode 100644 index 00000000..7d2c897e --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/quicksettings/widgets/Volume.ts @@ -0,0 +1,150 @@ +import { type Stream } from "types/service/audio" +import { Arrow, Menu } from "../ToggleButton" +import { dependencies, icon, sh } from "lib/utils" +import icons from "lib/icons.js" +const audio = await Service.import("audio") + +type Type = "microphone" | "speaker" + +const VolumeIndicator = (type: Type = "speaker") => Widget.Button({ + vpack: "center", + on_clicked: () => audio[type].is_muted = !audio[type].is_muted, + child: Widget.Icon({ + icon: audio[type].bind("icon_name") + .as(i => icon(i || "", icons.audio.mic.high)), + tooltipText: audio[type].bind("volume") + .as(vol => `Volume: ${Math.floor(vol * 100)}%`), + }), +}) + +const VolumeSlider = (type: Type = "speaker") => Widget.Slider({ + hexpand: true, + draw_value: false, + on_change: ({ value, dragging }) => { + if (dragging) { + audio[type].volume = value + audio[type].is_muted = false + } + }, + value: audio[type].bind("volume"), + class_name: audio[type].bind("is_muted").as(m => m ? "muted" : ""), +}) + +export const Volume = () => Widget.Box({ + class_name: "volume", + children: [ + VolumeIndicator("speaker"), + VolumeSlider("speaker"), + Widget.Box({ + vpack: "center", + child: Arrow("sink-selector"), + }), + Widget.Box({ + vpack: "center", + child: Arrow("app-mixer"), + visible: audio.bind("apps").as(a => a.length > 0), + }), + ], +}) + +export const Microphone = () => Widget.Box({ + class_name: "slider horizontal", + visible: audio.bind("recorders").as(a => a.length > 0), + children: [ + VolumeIndicator("microphone"), + VolumeSlider("microphone"), + ], +}) + +const MixerItem = (stream: Stream) => Widget.Box( + { + hexpand: true, + class_name: "mixer-item horizontal", + }, + Widget.Icon({ + tooltip_text: stream.bind("name").as(n => n || ""), + icon: stream.bind("name").as(n => { + return Utils.lookUpIcon(n || "") + ? (n || "") + : icons.fallback.audio + }), + }), + Widget.Box( + { vertical: true }, + Widget.Label({ + xalign: 0, + truncate: "end", + max_width_chars: 28, + label: stream.bind("description").as(d => d || ""), + }), + Widget.Slider({ + hexpand: true, + draw_value: false, + value: stream.bind("volume"), + on_change: ({ value }) => stream.volume = value, + }), + ), +) + +const SinkItem = (stream: Stream) => Widget.Button({ + hexpand: true, + on_clicked: () => audio.speaker = stream, + child: Widget.Box({ + children: [ + Widget.Icon({ + icon: icon(stream.icon_name || "", icons.fallback.audio), + tooltip_text: stream.icon_name || "", + }), + Widget.Label((stream.description || "").split(" ").slice(0, 4).join(" ")), + Widget.Icon({ + icon: icons.ui.tick, + hexpand: true, + hpack: "end", + visible: audio.speaker.bind("stream").as(s => s === stream.stream), + }), + ], + }), +}) + +const SettingsButton = () => Widget.Button({ + on_clicked: () => { + if (dependencies("pavucontrol")) + sh("pavucontrol") + }, + hexpand: true, + child: Widget.Box({ + children: [ + Widget.Icon(icons.ui.settings), + Widget.Label("Settings"), + ], + }), +}) + +export const AppMixer = () => Menu({ + name: "app-mixer", + icon: icons.audio.mixer, + title: "App Mixer", + content: [ + Widget.Box({ + vertical: true, + class_name: "vertical mixer-item-box", + children: audio.bind("apps").as(a => a.map(MixerItem)), + }), + Widget.Separator(), + SettingsButton(), + ], +}) + +export const SinkSelector = () => Menu({ + name: "sink-selector", + icon: icons.audio.type.headset, + title: "Sink Selector", + content: [ + Widget.Box({ + vertical: true, + children: audio.bind("speakers").as(a => a.map(SinkItem)), + }), + Widget.Separator(), + SettingsButton(), + ], +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/settings/Group.ts b/home/redyf/desktop/addons/ags/config/widget/settings/Group.ts new file mode 100644 index 00000000..e9356e04 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/settings/Group.ts @@ -0,0 +1,34 @@ +import icons from "lib/icons" +import Row from "./Row" + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default (title: string, ...rows: ReturnType>[]) => Widget.Box( + { + class_name: "group", + vertical: true, + }, + Widget.Box([ + Widget.Label({ + hpack: "start", + vpack: "end", + class_name: "group-title", + label: title, + setup: w => Utils.idle(() => w.visible = !!title), + }), + title ? Widget.Button({ + hexpand: true, + hpack: "end", + child: Widget.Icon(icons.ui.refresh), + class_name: "group-reset", + sensitive: Utils.merge( + rows.map(({ attribute: { opt } }) => opt.bind().as(v => v !== opt.initial)), + (...values) => values.some(b => b), + ), + on_clicked: () => rows.forEach(row => row.attribute.opt.reset()), + }) : Widget.Box(), + ]), + Widget.Box({ + vertical: true, + children: rows, + }), +) diff --git a/home/redyf/desktop/addons/ags/config/widget/settings/Page.ts b/home/redyf/desktop/addons/ags/config/widget/settings/Page.ts new file mode 100644 index 00000000..220e5608 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/settings/Page.ts @@ -0,0 +1,19 @@ +import Group from "./Group" + +export default ( + name: string, + icon: string, + ...groups: ReturnType>[] +) => Widget.Box({ + class_name: "page", + attribute: { name, icon }, + child: Widget.Scrollable({ + css: "min-height: 300px;", + child: Widget.Box({ + class_name: "page-content", + vexpand: true, + vertical: true, + children: groups, + }), + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/settings/Row.ts b/home/redyf/desktop/addons/ags/config/widget/settings/Row.ts new file mode 100644 index 00000000..1e170965 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/settings/Row.ts @@ -0,0 +1,55 @@ +import { Opt } from "lib/option" +import Setter from "./Setter" +import icons from "lib/icons" + +export type RowProps = { + opt: Opt + title: string + note?: string + type?: + | "number" + | "color" + | "float" + | "object" + | "string" + | "enum" + | "boolean" + | "img" + | "font" + enums?: string[] + max?: number + min?: number +} + +export default (props: RowProps) => Widget.Box( + { + attribute: { opt: props.opt }, + class_name: "row", + tooltip_text: props.note ? `note: ${props.note}` : "", + }, + Widget.Box( + { vertical: true, vpack: "center" }, + Widget.Label({ + xalign: 0, + class_name: "row-title", + label: props.title, + }), + Widget.Label({ + xalign: 0, + class_name: "id", + label: props.opt.id, + }), + ), + Widget.Box({ hexpand: true }), + Widget.Box( + { vpack: "center" }, + Setter(props), + ), + Widget.Button({ + vpack: "center", + class_name: "reset", + child: Widget.Icon(icons.ui.refresh), + on_clicked: () => props.opt.reset(), + sensitive: props.opt.bind().as(v => v !== props.opt.initial), + }), +) diff --git a/home/redyf/desktop/addons/ags/config/widget/settings/Setter.ts b/home/redyf/desktop/addons/ags/config/widget/settings/Setter.ts new file mode 100644 index 00000000..7e455c9e --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/settings/Setter.ts @@ -0,0 +1,93 @@ +import { type RowProps } from "./Row" +import { Opt } from "lib/option" +import icons from "lib/icons" +import Gdk from "gi://Gdk" + +function EnumSetter(opt: Opt, values: string[]) { + const lbl = Widget.Label({ label: opt.bind().as(v => `${v}`) }) + const step = (dir: 1 | -1) => { + const i = values.findIndex(i => i === lbl.label) + opt.setValue(dir > 0 + ? i + dir > values.length - 1 ? values[0] : values[i + dir] + : i + dir < 0 ? values[values.length - 1] : values[i + dir], + ) + } + const next = Widget.Button({ + child: Widget.Icon(icons.ui.arrow.right), + on_clicked: () => step(+1), + }) + const prev = Widget.Button({ + child: Widget.Icon(icons.ui.arrow.left), + on_clicked: () => step(-1), + }) + return Widget.Box({ + class_name: "enum-setter", + children: [lbl, prev, next], + }) +} + +export default function Setter({ + opt, + type = typeof opt.value as RowProps["type"], + enums, + max = 1000, + min = 0, +}: RowProps) { + switch (type) { + case "number": return Widget.SpinButton({ + setup(self) { + self.set_range(min, max) + self.set_increments(1, 5) + self.on("value-changed", () => opt.value = self.value as T) + self.hook(opt, () => self.value = opt.value as number) + }, + }) + + case "float": + case "object": return Widget.Entry({ + on_accept: self => opt.value = JSON.parse(self.text || ""), + setup: self => self.hook(opt, () => self.text = JSON.stringify(opt.value)), + }) + + case "string": return Widget.Entry({ + on_accept: self => opt.value = self.text as T, + setup: self => self.hook(opt, () => self.text = opt.value as string), + }) + + case "enum": return EnumSetter(opt as unknown as Opt, enums!) + case "boolean": return Widget.Switch() + .on("notify::active", self => opt.value = self.active as T) + .hook(opt, self => self.active = opt.value as boolean) + + case "img": return Widget.FileChooserButton({ + on_file_set: ({ uri }) => { opt.value = uri!.replace("file://", "") as T }, + }) + + case "font": return Widget.FontButton({ + show_size: false, + use_size: false, + setup: self => self + .hook(opt, () => self.font = opt.value as string) + .on("font-set", ({ font }) => opt.value = font! + .split(" ").slice(0, -1).join(" ") as T), + }) + + case "color": return Widget.ColorButton() + .hook(opt, self => { + const rgba = new Gdk.RGBA() + rgba.parse(opt.value as string) + self.rgba = rgba + }) + .on("color-set", ({ rgba: { red, green, blue } }) => { + const hex = (n: number) => { + const c = Math.floor(255 * n).toString(16) + return c.length === 1 ? `0${c}` : c + } + opt.value = `#${hex(red)}${hex(green)}${hex(blue)}` as T + }) + + default: return Widget.Label({ + label: `no setter with type ${type}`, + }) + } +} diff --git a/home/redyf/desktop/addons/ags/config/widget/settings/SettingsDialog.ts b/home/redyf/desktop/addons/ags/config/widget/settings/SettingsDialog.ts new file mode 100644 index 00000000..be0c35e7 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/settings/SettingsDialog.ts @@ -0,0 +1,63 @@ +import RegularWindow from "widget/RegularWindow" +import layout from "./layout" +import icons from "lib/icons" +import options from "options" + +const current = Variable(layout[0].attribute.name) + +const Header = () => Widget.CenterBox({ + class_name: "header", + start_widget: Widget.Button({ + class_name: "reset", + on_clicked: options.reset, + hpack: "start", + vpack: "start", + child: Widget.Icon(icons.ui.refresh), + tooltip_text: "Reset", + }), + center_widget: Widget.Box({ + class_name: "pager horizontal", + children: layout.map(({ attribute: { name, icon } }) => Widget.Button({ + xalign: 0, + class_name: current.bind().as(v => `${v === name ? "active" : ""}`), + on_clicked: () => current.value = name, + child: Widget.Box([ + Widget.Icon(icon), + Widget.Label(name), + ]), + })), + }), + end_widget: Widget.Button({ + class_name: "close", + hpack: "end", + vpack: "start", + child: Widget.Icon(icons.ui.close), + on_clicked: () => App.closeWindow("settings-dialog"), + }), +}) + +const PagesStack = () => Widget.Stack({ + transition: "slide_left_right", + children: layout.reduce((obj, page) => ({ ...obj, [page.attribute.name]: page }), {}), + shown: current.bind() as never, +}) + +export default () => RegularWindow({ + name: "settings-dialog", + class_name: "settings-dialog", + title: "Settings", + setup(win) { + win.on("delete-event", () => { + win.hide() + return true + }) + win.set_default_size(500, 600) + }, + child: Widget.Box({ + vertical: true, + children: [ + Header(), + PagesStack(), + ], + }), +}) diff --git a/home/redyf/desktop/addons/ags/config/widget/settings/Wallpaper.ts b/home/redyf/desktop/addons/ags/config/widget/settings/Wallpaper.ts new file mode 100644 index 00000000..998f3b7e --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/settings/Wallpaper.ts @@ -0,0 +1,31 @@ +import wallpaper from "service/wallpaper" + +export default () => Widget.Box( + { class_name: "row wallpaper" }, + Widget.Box( + { vertical: true }, + Widget.Label({ + xalign: 0, + class_name: "row-title", + label: "Wallpaper", + vpack: "start", + }), + Widget.Button({ + on_clicked: wallpaper.random, + label: "Random", + }), + Widget.FileChooserButton({ + on_file_set: ({ uri }) => wallpaper.set(uri!.replace("file://", "")), + }), + ), + Widget.Box({ hexpand: true }), + Widget.Box({ + class_name: "preview", + css: wallpaper.bind("wallpaper").as(wp => ` + min-height: 120px; + min-width: 200px; + background-image: url('${wp}'); + background-size: cover; + `), + }), +) diff --git a/home/redyf/desktop/addons/ags/config/widget/settings/layout.ts b/home/redyf/desktop/addons/ags/config/widget/settings/layout.ts new file mode 100644 index 00000000..ddd9dd55 --- /dev/null +++ b/home/redyf/desktop/addons/ags/config/widget/settings/layout.ts @@ -0,0 +1,149 @@ +/* eslint-disable max-len */ +import Row from "./Row" +import Group from "./Group" +import Page from "./Page" +import Wallpaper from "./Wallpaper" +import options from "options" +import icons from "lib/icons" + +const { + autotheme: at, + font, + theme, + bar: b, + launcher: l, + overview: ov, + powermenu: pm, + quicksettings: qs, + osd, + hyprland: h, +} = options + +const { + dark, + light, + blur, + scheme, + padding, + spacing, + radius, + shadows, + widget, + border, +} = theme + +export default [ + Page("Theme", icons.ui.themes, + Group("", + Wallpaper() as ReturnType, + Row({ opt: at, title: "Auto Generate Color Scheme" }), + Row({ opt: scheme, title: "Color Scheme", type: "enum", enums: ["dark", "light"] }), + ), + Group("Dark Colors", + Row({ opt: dark.bg, title: "Background", type: "color" }), + Row({ opt: dark.fg, title: "Foreground", type: "color" }), + Row({ opt: dark.primary.bg, title: "Primary", type: "color" }), + Row({ opt: dark.primary.fg, title: "On Primary", type: "color" }), + Row({ opt: dark.error.bg, title: "Error", type: "color" }), + Row({ opt: dark.error.fg, title: "On Error", type: "color" }), + Row({ opt: dark.widget, title: "Widget", type: "color" }), + Row({ opt: dark.border, title: "Border", type: "color" }), + ), + Group("Light Colors", + Row({ opt: light.bg, title: "Background", type: "color" }), + Row({ opt: light.fg, title: "Foreground", type: "color" }), + Row({ opt: light.primary.bg, title: "Primary", type: "color" }), + Row({ opt: light.primary.fg, title: "On Primary", type: "color" }), + Row({ opt: light.error.bg, title: "Error", type: "color" }), + Row({ opt: light.error.fg, title: "On Error", type: "color" }), + Row({ opt: light.widget, title: "Widget", type: "color" }), + Row({ opt: light.border, title: "Border", type: "color" }), + ), + Group("Theme", + Row({ opt: shadows, title: "Shadows" }), + Row({ opt: widget.opacity, title: "Widget Opacity", max: 100 }), + Row({ opt: border.opacity, title: "Border Opacity", max: 100 }), + Row({ opt: border.width, title: "Border Width" }), + Row({ opt: blur, title: "Blur", note: "0 to disable", max: 70 }), + ), + Group("UI", + Row({ opt: padding, title: "Padding" }), + Row({ opt: spacing, title: "Spacing" }), + Row({ opt: radius, title: "Roundness" }), + Row({ opt: font.size, title: "Font Size" }), + Row({ opt: font.name, title: "Font Name", type: "font" }), + ), + ), + Page("Bar", icons.ui.toolbars, + Group("General", + Row({ opt: b.transparent, title: "Transparent Bar", note: "Works best on empty-ish wallpapers" }), + Row({ opt: b.flatButtons, title: "Flat Buttons" }), + Row({ opt: b.position, title: "Position", type: "enum", enums: ["top", "bottom"] }), + Row({ opt: b.corners, title: "Corners" }), + ), + Group("Launcher", + Row({ opt: b.launcher.icon.icon, title: "Icon" }), + Row({ opt: b.launcher.icon.colored, title: "Colored Icon" }), + Row({ opt: b.launcher.label.label, title: "Label" }), + Row({ opt: b.launcher.label.colored, title: "Colored Label" }), + ), + Group("Workspaces", + Row({ opt: b.workspaces.workspaces, title: "Number of Workspaces", note: "0 to make it dynamic" }), + ), + Group("Taskbar", + Row({ opt: b.taskbar.iconSize, title: "Icon Size" }), + Row({ opt: b.taskbar.monochrome, title: "Monochrome" }), + Row({ opt: b.taskbar.exclusive, title: "Exclusive to workspaces" }), + ), + Group("Date", + Row({ opt: b.date.format, title: "Date Format" }), + ), + Group("Media", + Row({ opt: b.media.monochrome, title: "Monochrome" }), + Row({ opt: b.media.preferred, title: "Preferred Player" }), + Row({ opt: b.media.direction, title: "Slide Direction", type: "enum", enums: ["left", "right"] }), + Row({ opt: b.media.format, title: "Format of the Label" }), + Row({ opt: b.media.length, title: "Max Length of Label" }), + ), + Group("Battery", + Row({ opt: b.battery.bar, title: "Style", type: "enum", enums: ["hidden", "regular", "whole"] }), + Row({ opt: b.battery.blocks, title: "Number of Blocks" }), + Row({ opt: b.battery.width, title: "Width of Bar" }), + Row({ opt: b.battery.charging, title: "Charging Color", type: "color" }), + ), + Group("Powermenu", + Row({ opt: b.powermenu.monochrome, title: "Monochrome" }), + ), + ), + Page("General", icons.ui.settings, + Group("Hyprland", + Row({ opt: h.gapsWhenOnly, title: "Gaps When Only" }), + Row({ opt: h.inactiveBorder, type: "color", title: "Inactive Border Color" }), + ), + Group("Launcher", + Row({ opt: l.width, title: "Width" }), + Row({ opt: l.apps.iconSize, title: "Icon Size" }), + Row({ opt: l.apps.max, title: "Max Items" }), + ), + Group("Overview", + Row({ opt: ov.scale, title: "Scale", max: 100 }), + Row({ opt: ov.workspaces, title: "Workspaces", max: 11, note: "set this to 0 to make it dynamic" }), + Row({ opt: ov.monochromeIcon, title: "Monochrome Icons" }), + ), + Group("Powermenu", + Row({ opt: pm.layout, title: "Layout", type: "enum", enums: ["box", "line"] }), + Row({ opt: pm.labels, title: "Show Labels" }), + ), + Group("Quicksettings", + Row({ opt: qs.avatar.image, title: "Avatar", type: "img" }), + Row({ opt: qs.avatar.size, title: "Avatar Size" }), + Row({ opt: qs.media.monochromeIcon, title: "Media Monochrome Icons" }), + Row({ opt: qs.media.coverSize, title: "Media Cover Art Size" }), + ), + Group("On Screen Indicator", + Row({ opt: osd.progress.vertical, title: "Vertical" }), + Row({ opt: osd.progress.pack.h, title: "Horizontal Alignment", type: "enum", enums: ["start", "center", "end"] }), + Row({ opt: osd.progress.pack.v, title: "Vertical Alignment", type: "enum", enums: ["start", "center", "end"] }), + ), + ), +] as const diff --git a/home/redyf/desktop/addons/ags/default.nix b/home/redyf/desktop/addons/ags/default.nix new file mode 100644 index 00000000..1d69dce1 --- /dev/null +++ b/home/redyf/desktop/addons/ags/default.nix @@ -0,0 +1,33 @@ +{ + inputs, + pkgs, + ... +}: { + # add the home manager module + imports = [inputs.ags.homeManagerModules.default]; + + home.packages = with pkgs; [ + dart-sass + brightnessctl + inputs.matugen.packages.${system}.default + wf-recorder + wayshot + hyprpicker + pavucontrol + pamixer + ]; + + programs.ags = { + enable = true; + + # null or path, leave as null if you don't want hm to manage the config + configDir = ./config; + + # additional packages to add to gjs's runtime + extraPackages = with pkgs; [ + gtksourceview + webkitgtk + accountsservice + ]; + }; +} diff --git a/home/redyf/desktop/addons/default.nix b/home/redyf/desktop/addons/default.nix index be869086..f4ad9f9b 100644 --- a/home/redyf/desktop/addons/default.nix +++ b/home/redyf/desktop/addons/default.nix @@ -1,11 +1,12 @@ _: { imports = [ - # ./alacritty - # ./foot + ./ags ./kitty - # ./rofi ./swww # ./waybar + # ./alacritty + # ./foot + # ./rofi # ./wofi ]; } diff --git a/home/redyf/desktop/addons/kitty/default.nix b/home/redyf/desktop/addons/kitty/default.nix index d8477006..846088d4 100644 --- a/home/redyf/desktop/addons/kitty/default.nix +++ b/home/redyf/desktop/addons/kitty/default.nix @@ -90,8 +90,10 @@ "ctrl+shift+c" = "copy_to_clipboard"; "ctrl+shift+v" = "paste_from_clipboard"; "ctrl+shift+s" = "paste_from_selection"; + "ctrl+shift+e" = "open_url"; "ctrl+shift+=" = "increase_font_size"; "ctrl+shift+-" = "decrease_font_size"; + "ctrl+shift+backspace" = "restore_font_size"; "ctrl+shift+up" = "scroll_line_up"; "ctrl+shift+k" = "scroll_line_up"; "ctrl+shift+down" = "scroll_line_down"; @@ -102,8 +104,8 @@ "ctrl+shift+[" = "previous_window"; "ctrl+shift+right" = "next_tab"; "ctrl+tab" = "next_tab"; - "ctrl+shift+left" = "previous_tab"; "ctrl+shift+tab" = "previous_tab"; + "ctrl+shift+left" = "previous_tab"; "ctrl+shift+t" = "new_tab"; "ctrl+shift+q" = "close_tab"; }; diff --git a/home/redyf/desktop/addons/waybar/default.nix b/home/redyf/desktop/addons/waybar/default.nix index 59e17111..30a773f6 100644 --- a/home/redyf/desktop/addons/waybar/default.nix +++ b/home/redyf/desktop/addons/waybar/default.nix @@ -12,14 +12,14 @@ # waybar_config = import ./nixbar/config.nix {inherit osConfig config lib pkgs;}; # waybar_style = import ./nixbar/style.nix {inherit (config) colorscheme;}; # Tokyonight - waybar_config = import ./tokyonight/config.nix {inherit osConfig config lib pkgs;}; - waybar_style = import ./tokyonight/style.nix {inherit (config) colorscheme;}; + # waybar_config = import ./tokyonight/config.nix {inherit osConfig config lib pkgs;}; + # waybar_style = import ./tokyonight/style.nix {inherit (config) colorscheme;}; # Catppuccin - # waybar_config = import ./catppuccin/config.nix { inherit osConfig config lib pkgs; }; - # waybar_style = import ./catppuccin/style.nix { inherit (config) colorscheme; }; + # waybar_config = import ./catppuccin/config.nix {inherit osConfig config lib pkgs;}; + # waybar_style = import ./catppuccin/style.nix {inherit (config) colorscheme;}; # Simple bar - # waybar_config = import ./simple/config.nix {inherit osConfig config lib pkgs;}; - # waybar_style = import ./simple/style.nix {inherit (config) colorscheme;}; + waybar_config = import ./simple/config.nix {inherit osConfig config lib pkgs;}; + waybar_style = import ./simple/style.nix {inherit (config) colorscheme;}; in { programs.waybar = { enable = true; diff --git a/home/redyf/desktop/addons/waybar/simple/config.nix b/home/redyf/desktop/addons/waybar/simple/config.nix index ccb11403..2ceedadb 100644 --- a/home/redyf/desktop/addons/waybar/simple/config.nix +++ b/home/redyf/desktop/addons/waybar/simple/config.nix @@ -15,7 +15,7 @@ calendar = { format = {today = "{}";}; }; - format = " {:%H:%M}"; + format = " {:%H:%M}"; tooltip = "true"; tooltip-format = '' {:%Y %B} @@ -44,13 +44,13 @@ }; memory = { - format = " {}%"; + format = " {}%"; format-alt = " {used} GB"; interval = 2; }; cpu = { - format = " {usage}%"; + format = " {usage}%"; format-alt = " {avg_frequency} GHz"; interval = 2; }; @@ -61,7 +61,7 @@ }; pulseaudio = { - format = "{icon} {volume}%"; + format = "{icon} {volume}%"; format-muted = "󰖁 "; format-icons = {default = [" "];}; scroll-step = 5; diff --git a/home/redyf/desktop/addons/waybar/simple/style.nix b/home/redyf/desktop/addons/waybar/simple/style.nix index d2a9439b..a4980b33 100644 --- a/home/redyf/desktop/addons/waybar/simple/style.nix +++ b/home/redyf/desktop/addons/waybar/simple/style.nix @@ -5,13 +5,13 @@ _: '' padding: 0; margin: 0; min-height: 0px; - font-family: MonoLisa; + font-family: FiraCode Nerd Font; font-weight: bold; opacity: 0.98; } window#waybar { - background: none; + background: transparent; } #workspaces { diff --git a/home/redyf/desktop/gtk/default.nix b/home/redyf/desktop/gtk/default.nix index b98d66cd..11d1d6fc 100644 --- a/home/redyf/desktop/gtk/default.nix +++ b/home/redyf/desktop/gtk/default.nix @@ -7,17 +7,6 @@ size = 32; # Affects gtk applications as the name suggests }; - # theme = { - # name = "Catppuccin-Macchiato-Compact-Blue-dark"; - # package = pkgs.catppuccin-gtk.override { - # size = "compact"; - # accents = ["blue"]; - # variant = "macchiato"; - # }; - # name = "WhiteSur"; - # package = pkgs.whitesur-gtk-theme; - # }; - iconTheme = { name = "Papirus-Dark"; package = pkgs.papirus-folders; @@ -25,4 +14,17 @@ # package = pkgs.whitesur-icon-theme; }; }; + + stylix = { + targets = { + bemenu = { + enable = true; + alternate = true; + fontSize = 14; + }; + tmux.enable = false; + mako.enable = true; + vesktop.enable = true; + }; + }; } diff --git a/home/redyf/desktop/hyprland/default.nix b/home/redyf/desktop/hyprland/default.nix index 2e4dedac..c13a60e6 100644 --- a/home/redyf/desktop/hyprland/default.nix +++ b/home/redyf/desktop/hyprland/default.nix @@ -37,6 +37,9 @@ in { $scripts/launch_waybar & $scripts/tools/dynamic & + # ags (bar and some extra stuff) + ags + # Wallpaper swww kill swww init diff --git a/home/redyf/system/fonts/default.nix b/home/redyf/system/fonts/default.nix index c6f72136..add45c37 100644 --- a/home/redyf/system/fonts/default.nix +++ b/home/redyf/system/fonts/default.nix @@ -8,9 +8,10 @@ (nerdfonts.override {fonts = ["JetBrainsMono"];}) noto-fonts powerline-symbols - monolisa-script + # monolisa-script + # berkeley # sf-mono-liga-bin - # geist-font + # geist-mono ]; }; } diff --git a/hosts/redyf/configuration.nix b/hosts/redyf/configuration.nix index bd477e39..09b8524b 100644 --- a/hosts/redyf/configuration.nix +++ b/hosts/redyf/configuration.nix @@ -128,18 +128,6 @@ in { }; }; - # fonts = { - # enableDefaultPackages = true; - # fontconfig = { - # enable = true; - # defaultFonts = { - # serif = ["Times, Noto Serif"]; - # sansSerif = ["Helvetica Neue LT Std, Helvetica, Noto Sans"]; - # monospace = ["Courier Prime, Courier, Noto Sans Mono"]; - # }; - # }; - # }; - programs = { zsh.enable = true; hyprland = { @@ -157,12 +145,12 @@ in { stylix = { autoEnable = true; - image = "./lain05.jpg"; + image = ./lain05.jpg; base16Scheme = "${pkgs.base16-schemes}/share/themes/${theme}.yaml"; fonts = { monospace = { - package = with pkgs; nerdfonts.override {fonts = ["JetBrainsMono"];}; - name = "JetBrainsMono Nerd Font"; + package = with pkgs; nerdfonts.override {fonts = ["FiraCode"];}; + name = "FiraCode Nerd Font"; }; sansSerif = { package = pkgs.dejavu_fonts; @@ -174,7 +162,7 @@ in { }; sizes = { applications = 10; - terminal = 11; + terminal = 14; desktop = 10; popups = 11; };