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 @@
+
+
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;
};