aboutsummaryrefslogtreecommitdiff
path: root/.config/ags
diff options
context:
space:
mode:
authorsrdusr <trevorgray@srdusr.com>2024-06-13 13:11:05 +0200
committersrdusr <trevorgray@srdusr.com>2024-06-13 13:11:05 +0200
commitd0fbb19623e4fb6097e1ff3ee49c6a76a0928d0e (patch)
tree937531ddf423d3935c6e20c8a9227e39ce782241 /.config/ags
parent4ccbe0270c25ecab492508b5b0209ae53b9c35bd (diff)
downloaddotfiles-d0fbb19623e4fb6097e1ff3ee49c6a76a0928d0e.tar.gz
dotfiles-d0fbb19623e4fb6097e1ff3ee49c6a76a0928d0e.zip
Add ags
Diffstat (limited to '.config/ags')
-rw-r--r--.config/ags/.eslintrc.yml130
-rw-r--r--.config/ags/.gitignore6
-rw-r--r--.config/ags/assets/arrows-down.svg10
-rw-r--r--.config/ags/assets/arrows-left.svg10
-rw-r--r--.config/ags/assets/arrows-right.svg10
-rw-r--r--.config/ags/assets/arrows-up.svg10
-rw-r--r--.config/ags/assets/battery-flash-symbolic.svg4
-rw-r--r--.config/ags/assets/bomb-kill.svg36
-rw-r--r--.config/ags/assets/chat-bubbles-symbolic.svg5
-rw-r--r--.config/ags/assets/controller-symbolic.svg4
-rw-r--r--.config/ags/assets/controls-symbolic.svg5
-rw-r--r--.config/ags/assets/dark-mode-symbolic.svg4
-rw-r--r--.config/ags/assets/float.svg44
-rw-r--r--.config/ags/assets/fullscreen.svg43
-rw-r--r--.config/ags/assets/hourglass-symbolic.svg4
-rw-r--r--.config/ags/assets/light-mode-symbolic.svg4
-rw-r--r--.config/ags/assets/mixer-symbolic.svg6
-rw-r--r--.config/ags/assets/nix-snowflake-symbolic.svg155
-rw-r--r--.config/ags/assets/nixos-symbolic.svg155
-rw-r--r--.config/ags/assets/nixos.svg277
-rw-r--r--.config/ags/assets/osk.svg132
-rw-r--r--.config/ags/assets/pinned.svg36
-rw-r--r--.config/ags/assets/preferences-desktop-theme-symbolic.svg321
-rw-r--r--.config/ags/assets/processor-symbolic.svg17
-rw-r--r--.config/ags/assets/rotation.svg8
-rw-r--r--.config/ags/assets/swapnext.svg8
-rw-r--r--.config/ags/assets/tbox-close.svg49
-rw-r--r--.config/ags/assets/terminal-symbolic.svg5
-rw-r--r--.config/ags/assets/togglesplit.svg10
-rw-r--r--.config/ags/assets/toolbars-symbolic.svg4
-rw-r--r--.config/ags/assets/wp-next.svg10
-rw-r--r--.config/ags/assets/wp-prev.svg10
-rw-r--r--.config/ags/config.js46
-rw-r--r--.config/ags/default.nix104
-rw-r--r--.config/ags/greeter.js18
-rw-r--r--.config/ags/greeter/auth.ts109
-rw-r--r--.config/ags/greeter/greeter.scss64
-rw-r--r--.config/ags/greeter/greeter.ts37
-rw-r--r--.config/ags/greeter/session.ts20
-rw-r--r--.config/ags/greeter/statusbar.ts46
-rw-r--r--.config/ags/lib/battery.ts16
-rw-r--r--.config/ags/lib/client.js134
-rw-r--r--.config/ags/lib/cursorhover.js86
-rw-r--r--.config/ags/lib/gtk.ts16
-rw-r--r--.config/ags/lib/hyprland.ts80
-rw-r--r--.config/ags/lib/iconUtils.js48
-rw-r--r--.config/ags/lib/icons.ts185
-rw-r--r--.config/ags/lib/init.ts19
-rw-r--r--.config/ags/lib/matugen.ts113
-rw-r--r--.config/ags/lib/notifications.ts16
-rw-r--r--.config/ags/lib/option.ts115
-rw-r--r--.config/ags/lib/session.ts16
-rw-r--r--.config/ags/lib/tmux.ts14
-rw-r--r--.config/ags/lib/utils.ts111
-rw-r--r--.config/ags/lib/variables.ts47
-rw-r--r--.config/ags/main.ts47
-rw-r--r--.config/ags/options.ts267
-rw-r--r--.config/ags/package.json18
-rw-r--r--.config/ags/service/asusctl.ts52
-rw-r--r--.config/ags/service/brightness.ts69
-rw-r--r--.config/ags/service/colorpicker.ts56
-rw-r--r--.config/ags/service/nix.ts109
-rw-r--r--.config/ags/service/powermenu.ts43
-rw-r--r--.config/ags/service/screenrecord.ts102
-rw-r--r--.config/ags/service/wallpaper.ts98
-rw-r--r--.config/ags/service/weather.ts59
-rw-r--r--.config/ags/style/extra.scss67
-rw-r--r--.config/ags/style/mixins/a11y-button.scss48
-rw-r--r--.config/ags/style/mixins/button.scss70
-rw-r--r--.config/ags/style/mixins/floating-widget.scss12
-rw-r--r--.config/ags/style/mixins/hidden.scss15
-rw-r--r--.config/ags/style/mixins/media.scss42
-rw-r--r--.config/ags/style/mixins/scrollable.scss42
-rw-r--r--.config/ags/style/mixins/slider.scss74
-rw-r--r--.config/ags/style/mixins/spacing.scss53
-rw-r--r--.config/ags/style/mixins/switch.scss16
-rw-r--r--.config/ags/style/mixins/unset.scss9
-rw-r--r--.config/ags/style/mixins/widget.scss7
-rw-r--r--.config/ags/style/style.ts103
-rw-r--r--.config/ags/tsconfig.json19
-rw-r--r--.config/ags/widget/PopupWindow.ts156
-rw-r--r--.config/ags/widget/RegularWindow.ts3
-rw-r--r--.config/ags/widget/bar/Bar.ts57
-rw-r--r--.config/ags/widget/bar/PanelButton.ts46
-rw-r--r--.config/ags/widget/bar/ScreenCorners.ts25
-rw-r--r--.config/ags/widget/bar/bar.scss242
-rw-r--r--.config/ags/widget/bar/buttons/BatteryBar.ts94
-rw-r--r--.config/ags/widget/bar/buttons/ColorPicker.ts37
-rw-r--r--.config/ags/widget/bar/buttons/Date.ts15
-rw-r--r--.config/ags/widget/bar/buttons/Launcher.ts49
-rw-r--r--.config/ags/widget/bar/buttons/Media.ts92
-rw-r--r--.config/ags/widget/bar/buttons/Messages.ts16
-rw-r--r--.config/ags/widget/bar/buttons/PowerMenu.ts15
-rw-r--r--.config/ags/widget/bar/buttons/ScreenRecord.ts21
-rw-r--r--.config/ags/widget/bar/buttons/SysTray.ts39
-rw-r--r--.config/ags/widget/bar/buttons/SystemIndicators.ts107
-rw-r--r--.config/ags/widget/bar/buttons/Taskbar.ts90
-rw-r--r--.config/ags/widget/bar/buttons/Workspaces.ts38
-rw-r--r--.config/ags/widget/bar/screencorner.scss51
-rw-r--r--.config/ags/widget/datemenu/DateColumn.ts37
-rw-r--r--.config/ags/widget/datemenu/DateMenu.ts36
-rw-r--r--.config/ags/widget/datemenu/NotificationColumn.ts113
-rw-r--r--.config/ags/widget/datemenu/datemenu.scss110
-rw-r--r--.config/ags/widget/desktop/Desktop.ts40
-rw-r--r--.config/ags/widget/dock/Dock.ts150
-rw-r--r--.config/ags/widget/dock/FloatingDock.ts78
-rw-r--r--.config/ags/widget/dock/ToolBox.ts122
-rw-r--r--.config/ags/widget/dock/ToolBoxDock.ts57
-rw-r--r--.config/ags/widget/dock/dock.scss73
-rw-r--r--.config/ags/widget/launcher/AppLauncher.ts130
-rw-r--r--.config/ags/widget/launcher/Launcher.ts139
-rw-r--r--.config/ags/widget/launcher/NixRun.ts118
-rw-r--r--.config/ags/widget/launcher/ShRun.ts89
-rw-r--r--.config/ags/widget/launcher/launcher.scss143
-rw-r--r--.config/ags/widget/notifications/Notification.ts138
-rw-r--r--.config/ags/widget/notifications/NotificationPopups.ts90
-rw-r--r--.config/ags/widget/notifications/notifications.scss79
-rw-r--r--.config/ags/widget/osd/OSD.ts111
-rw-r--r--.config/ags/widget/osd/Progress.ts74
-rw-r--r--.config/ags/widget/osd/osd.scss26
-rw-r--r--.config/ags/widget/overview/Overview.ts41
-rw-r--r--.config/ags/widget/overview/Window.ts48
-rw-r--r--.config/ags/widget/overview/Workspace.ts76
-rw-r--r--.config/ags/widget/overview/overview.scss34
-rw-r--r--.config/ags/widget/powermenu/PowerMenu.ts56
-rw-r--r--.config/ags/widget/powermenu/Verification.ts47
-rw-r--r--.config/ags/widget/powermenu/powermenu.scss110
-rw-r--r--.config/ags/widget/quicksettings/QuickSettings.ts84
-rw-r--r--.config/ags/widget/quicksettings/ToggleButton.ts154
-rw-r--r--.config/ags/widget/quicksettings/quicksettings.scss177
-rw-r--r--.config/ags/widget/quicksettings/widgets/Bluetooth.ts61
-rw-r--r--.config/ags/widget/quicksettings/widgets/Brightness.ts23
-rw-r--r--.config/ags/widget/quicksettings/widgets/DND.ts12
-rw-r--r--.config/ags/widget/quicksettings/widgets/DarkMode.ts12
-rw-r--r--.config/ags/widget/quicksettings/widgets/Header.ts69
-rw-r--r--.config/ags/widget/quicksettings/widgets/Media.ts153
-rw-r--r--.config/ags/widget/quicksettings/widgets/MicMute.ts18
-rw-r--r--.config/ags/widget/quicksettings/widgets/Network.ts61
-rw-r--r--.config/ags/widget/quicksettings/widgets/PowerProfile.ts99
-rw-r--r--.config/ags/widget/quicksettings/widgets/Volume.ts150
-rw-r--r--.config/ags/widget/settings/Group.ts34
-rw-r--r--.config/ags/widget/settings/Page.ts19
-rw-r--r--.config/ags/widget/settings/Row.ts55
-rw-r--r--.config/ags/widget/settings/Setter.ts93
-rw-r--r--.config/ags/widget/settings/SettingsDialog.ts63
-rw-r--r--.config/ags/widget/settings/Wallpaper.ts31
-rw-r--r--.config/ags/widget/settings/layout.ts147
-rw-r--r--.config/ags/widget/settings/settingsdialog.scss144
148 files changed, 9706 insertions, 0 deletions
diff --git a/.config/ags/.eslintrc.yml b/.config/ags/.eslintrc.yml
new file mode 100644
index 0000000..ff96a83
--- /dev/null
+++ b/.config/ags/.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/.config/ags/.gitignore b/.config/ags/.gitignore
new file mode 100644
index 0000000..f56dbd1
--- /dev/null
+++ b/.config/ags/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+types
+package-lock.json
+bun.lockb
+flake.lock
+.weather
diff --git a/.config/ags/assets/arrows-down.svg b/.config/ags/assets/arrows-down.svg
new file mode 100644
index 0000000..5851aed
--- /dev/null
+++ b/.config/ags/assets/arrows-down.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g transform="matrix(1,0,0,1,4,4)">
+ <path class="ColorScheme-Text" d="M 7,2 V 10 L 3.5,6.5 2,8 8,14 14,8 12.5,6.5 9,10 V 2 Z" style="fill:currentColor"/>
+ </g>
+</svg>
diff --git a/.config/ags/assets/arrows-left.svg b/.config/ags/assets/arrows-left.svg
new file mode 100644
index 0000000..f3c1b2e
--- /dev/null
+++ b/.config/ags/assets/arrows-left.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g transform="matrix(1,0,0,1,4,4)">
+ <path class="ColorScheme-Text" d="M 14,7 H 6 L 9.5,3.5 8,2 2,8 8,14 9.5,12.5 6,9 H 14 Z" style="fill:currentColor"/>
+ </g>
+</svg>
diff --git a/.config/ags/assets/arrows-right.svg b/.config/ags/assets/arrows-right.svg
new file mode 100644
index 0000000..30baa8f
--- /dev/null
+++ b/.config/ags/assets/arrows-right.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g transform="matrix(1,0,0,1,4,4)">
+ <path class="ColorScheme-Text" d="M 2,9 H 10 L 6.5,12.5 8,14 14,8 8,2 6.5,3.5 10,7 H 2 Z" style="fill:currentColor"/>
+ </g>
+</svg>
diff --git a/.config/ags/assets/arrows-up.svg b/.config/ags/assets/arrows-up.svg
new file mode 100644
index 0000000..68b9944
--- /dev/null
+++ b/.config/ags/assets/arrows-up.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g transform="matrix(1,0,0,1,4,4)">
+ <path class="ColorScheme-Text" d="M 7,14 V 6 L 3.5,9.5 2,8 8,2 14,8 12.5,9.5 9,6 V 14 Z" style="fill:currentColor"/>
+ </g>
+</svg>
diff --git a/.config/ags/assets/battery-flash-symbolic.svg b/.config/ags/assets/battery-flash-symbolic.svg
new file mode 100644
index 0000000..21b5e33
--- /dev/null
+++ b/.config/ags/assets/battery-flash-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 8.96875 0 c -0.332031 0.0117188 -0.640625 0.1875 -0.816406 0.46875 l -5 8 c -0.105469 0.171875 -0.152344 0.355469 -0.152344 0.53125 v 1 h 3 v 5 c 0 1.003906 1.316406 1.378906 1.847656 0.53125 l 5 -8 c 0.105469 -0.171875 0.152344 -0.355469 0.152344 -0.53125 v -1 h -3 v -5 c 0 -0.5625 -0.464844 -1.015625 -1.03125 -1 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/bomb-kill.svg b/.config/ags/assets/bomb-kill.svg
new file mode 100644
index 0000000..a6e9f09
--- /dev/null
+++ b/.config/ags/assets/bomb-kill.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ viewBox="0 0 22 22"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="bomb-kill.svg"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="28.545455"
+ inkscape:cx="11"
+ inkscape:cy="11"
+ inkscape:current-layer="svg1" />
+ <defs
+ id="defs3051">
+ <style
+ type="text/css"
+ id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
+ </defs>
+ <path
+ style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:0.855269"
+ d="m 17.858728,4.1743879 c -1.13103,0.2314779 -2.107737,0.9080479 -2.727751,1.858046 -0.214576,-0.095764 -0.450425,-0.1516429 -0.701613,-0.1516429 -0.178764,0 -0.347261,0.034927 -0.509046,0.084986 -0.975172,-0.5918778 -2.11854,-0.9381881 -3.348989,-0.9381881 -3.5622531,0 -6.4300571,2.8539574 -6.4300571,6.3990111 0,3.545055 2.867804,6.399011 6.4300571,6.399011 3.562254,0 6.430058,-2.853956 6.430058,-6.39901 0,-1.224507 -0.347991,-2.3623553 -0.94274,-3.3328191 0.0503,-0.1610043 0.0854,-0.3286874 0.0854,-0.5065884 0,-0.3822616 -0.128156,-0.7309813 -0.339922,-1.0148431 0.459139,-0.7437673 1.188057,-1.2957452 2.054604,-1.5197652 V 4.1743879 M 6.9862372,7.1755908 C 6.173802,8.138787 5.6851553,9.38169 5.6851553,10.743373 c 0,3.07238 2.4854307,5.54581 5.5727167,5.54581 1.36784,0 2.61568,-0.486764 3.583418,-1.2948 -1.020672,1.209625 -2.551332,1.978028 -4.269961,1.978028 -3.0872862,0 -5.5727161,-2.47343 -5.5727161,-5.54581 0,-1.7106949 0.7716789,-3.2352661 1.9876243,-4.2510102"
+ class="ColorScheme-Text"
+ id="path1" />
+</svg>
diff --git a/.config/ags/assets/chat-bubbles-symbolic.svg b/.config/ags/assets/chat-bubbles-symbolic.svg
new file mode 100644
index 0000000..fdee0b3
--- /dev/null
+++ b/.config/ags/assets/chat-bubbles-symbolic.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 14 3.175781 v 3.824219 c 0 2.179688 -1.820312 4 -4 4 h -3.585938 l -2 2 h 5.585938 l 3 3 v -3 c 1.644531 0 3 -1.355469 3 -3 v -4 c 0 -1.292969 -0.839844 -2.40625 -2 -2.824219 z m 0 0" fill-opacity="0.34902"/>
+ <path d="m 3 0 c -1.644531 0 -3 1.355469 -3 3 v 4 c 0 1.644531 1.355469 3 3 3 v 3 l 3 -3 h 4 c 1.644531 0 3 -1.355469 3 -3 v -4 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/controller-symbolic.svg b/.config/ags/assets/controller-symbolic.svg
new file mode 100644
index 0000000..98bf5d6
--- /dev/null
+++ b/.config/ags/assets/controller-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 3.785156 2.03125 c -0.242187 0 -0.523437 0.066406 -0.804687 0.21875 c -1.039063 0.546875 -1.992188 2.335938 -2.511719 4.65625 c -0.4414062 1.972656 -0.605469 4.664062 -0.339844 5.75 c 0.226563 0.933594 0.625 1.34375 1.332032 1.34375 c 1.042968 -0.019531 2.359374 -1.183594 3.191406 -2.75 c 0.601562 -0.867188 2 -1.261719 3.347656 -1.21875 c 1.347656 -0.046875 2.746094 0.351562 3.347656 1.21875 c 0.832032 1.566406 2.148438 2.730469 3.191406 2.75 c 0.707032 0 1.105469 -0.410156 1.332032 -1.34375 c 0.265625 -1.085938 0.101562 -3.777344 -0.339844 -5.75 c -0.519531 -2.320312 -1.472656 -4.109375 -2.511719 -4.65625 c -0.566406 -0.304688 -1.039062 -0.296875 -1.453125 0 c -0.527344 0.375 -1.628906 0.78125 -3.566406 0.78125 c -1.9375 0.003906 -3.039062 -0.40625 -3.566406 -0.78125 c -0.207032 -0.148438 -0.40625 -0.21875 -0.648438 -0.21875 z m 0.246094 3 h 0.992188 v 1 h 0.992187 v 1 h -0.992187 v 1 h -0.992188 v -1 h -0.992188 v -1 h 0.992188 z m 7.441406 0 c 0.273438 0 0.496094 0.222656 0.496094 0.5 s -0.222656 0.5 -0.496094 0.5 c -0.273437 0 -0.496094 -0.222656 -0.496094 -0.5 s 0.222657 -0.5 0.496094 -0.5 z m -0.992187 1 c 0.273437 0 0.496093 0.222656 0.496093 0.5 s -0.222656 0.5 -0.496093 0.5 c -0.273438 0 -0.496094 -0.222656 -0.496094 -0.5 s 0.222656 -0.5 0.496094 -0.5 z m 1.984375 0 c 0.273437 0 0.496094 0.222656 0.496094 0.5 s -0.222657 0.5 -0.496094 0.5 c -0.273438 0 -0.496094 -0.222656 -0.496094 -0.5 s 0.222656 -0.5 0.496094 -0.5 z m -0.992188 1 c 0.273438 0 0.496094 0.222656 0.496094 0.5 s -0.222656 0.5 -0.496094 0.5 c -0.273437 0 -0.496094 -0.222656 -0.496094 -0.5 s 0.222657 -0.5 0.496094 -0.5 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/controls-symbolic.svg b/.config/ags/assets/controls-symbolic.svg
new file mode 100644
index 0000000..7df5663
--- /dev/null
+++ b/.config/ags/assets/controls-symbolic.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 4.550781 1 c -1.9375 0 -3.5 1.5625 -3.5 3.5 s 1.5625 3.5 3.5 3.5 h 7 c 1.941407 0 3.5 -1.5625 3.5 -3.5 s -1.558593 -3.5 -3.5 -3.5 z m 7 1 c 1.386719 0 2.5 1.113281 2.5 2.5 c 0 1.382812 -1.113281 2.5 -2.5 2.5 c -1.382812 0 -2.5 -1.117188 -2.5 -2.5 c 0 -1.386719 1.117188 -2.5 2.5 -2.5 z m 0 0"/>
+ <path d="m 4.550781 9 c -1.9375 0 -3.5 1.5625 -3.5 3.5 s 1.5625 3.5 3.5 3.5 h 7 c 1.941407 0 3.5 -1.5625 3.5 -3.5 s -1.558593 -3.5 -3.5 -3.5 z m 0 1 c 1.386719 0 2.5 1.113281 2.5 2.5 c 0 1.382812 -1.113281 2.5 -2.5 2.5 c -1.382812 0 -2.5 -1.117188 -2.5 -2.5 c 0 -1.386719 1.117188 -2.5 2.5 -2.5 z m 0 0" fill-opacity="0.34902"/>
+</svg>
diff --git a/.config/ags/assets/dark-mode-symbolic.svg b/.config/ags/assets/dark-mode-symbolic.svg
new file mode 100644
index 0000000..9f2e6b4
--- /dev/null
+++ b/.config/ags/assets/dark-mode-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 0.917969 8.003906 c 0 3.914063 3.164062 7.078125 7.078125 7.078125 c 3.605468 -0.007812 6.617187 -2.703125 7.023437 -6.285156 c 0.042969 -0.378906 -0.136719 -0.75 -0.457031 -0.957031 c -0.324219 -0.203125 -0.738281 -0.207032 -1.0625 -0.003906 c -0.609375 0.375 -1.316406 0.578124 -2.03125 0.578124 c -2.140625 0 -3.882812 -1.742187 -3.882812 -3.882812 c 0 -0.714844 0.203124 -1.421875 0.578124 -2.03125 c 0.203126 -0.324219 0.199219 -0.738281 -0.003906 -1.0625 c -0.207031 -0.320312 -0.578125 -0.5 -0.957031 -0.457031 c -3.582031 0.40625 -6.277344 3.417969 -6.285156 7.023437 z m 4.667969 -3.472656 c 0 3.253906 2.628906 5.882812 5.886718 5.882812 c 1.085938 0 2.152344 -0.304687 3.078125 -0.878906 l -1.519531 -0.960937 c -0.289062 2.554687 -2.464844 4.503906 -5.035156 4.507812 c -2.796875 0 -5.078125 -2.28125 -5.078125 -5.078125 c 0.003906 -2.570312 1.953125 -4.746094 4.507812 -5.035156 l -0.960937 -1.519531 c -0.574219 0.925781 -0.875 1.992187 -0.878906 3.082031 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/float.svg b/.config/ags/assets/float.svg
new file mode 100644
index 0000000..dc8078a
--- /dev/null
+++ b/.config/ags/assets/float.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ viewBox="0 0 16 16"
+ version="1.1"
+ id="svg2"
+ sodipodi:docname="float.svg"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview2"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="39.25"
+ inkscape:cx="8"
+ inkscape:cy="8"
+ inkscape:current-layer="svg2" />
+ <defs
+ id="defs1">
+ <style
+ type="text/css"
+ id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
+ </defs>
+ <path
+ class="ColorScheme-Text"
+ d="M 7.8252389,2.2944865 A 5.6426155,5.4275975 0 0 0 2.1826234,7.722084 5.6426155,5.4275975 0 0 0 7.8252389,13.149682 5.6426155,5.4275975 0 0 0 13.467855,7.722084 5.6426155,5.4275975 0 0 0 7.8252389,2.2944865 m 0,0.9045996 A 4.7021795,4.5229979 0 0 1 12.527419,7.722084 4.7021795,4.5229979 0 0 1 7.8252389,12.245082 4.7021795,4.5229979 0 0 1 3.1230593,7.722084 4.7021795,4.5229979 0 0 1 7.8252389,3.1990861"
+ fill="currentColor"
+ id="path1"
+ style="stroke-width:0.922343" />
+ <path
+ class="ColorScheme-Text"
+ d="M 12.52741,7.7220642 A 4.7021795,4.5229979 0 0 1 7.8252308,12.245062 4.7021795,4.5229979 0 0 1 3.1230512,7.7220642 4.7021795,4.5229979 0 0 1 7.8252308,3.1990663 4.7021795,4.5229979 0 0 1 12.52741,7.7220642 Z"
+ fill="currentColor"
+ fill-opacity="0.5"
+ id="path2"
+ style="stroke-width:0.922343" />
+</svg>
diff --git a/.config/ags/assets/fullscreen.svg b/.config/ags/assets/fullscreen.svg
new file mode 100644
index 0000000..e9ab0b8
--- /dev/null
+++ b/.config/ags/assets/fullscreen.svg
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="24"
+ height="24"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="fullscreen.svg"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="19.666667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:current-layer="svg1" />
+ <defs
+ id="defs1">
+ <style
+ id="current-color-scheme"
+ type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g
+ transform="matrix(0.94763804,0,0,0.95290155,4.4188957,4.3767876)"
+ id="g1">
+ <path
+ style="fill:currentColor"
+ class="ColorScheme-Text"
+ d="M 4,10 1,8 4,6 Z m 8,0 3,-2 -3,-2 z m -4,5 2,-3 H 6 Z M 8,1 10,4 H 6 Z m 7,6 h 1 V 9 H 15 Z M 0,7 H 1 V 9 H 0 Z m 7,8 h 2 v 1 H 7 Z M 7,0 H 9 V 1 H 7 Z M 0,16 v -5 h 1 v 4 h 4 v 1 z m 16,0 v -4 h -1 v 3 h -4 v 1 z M 16,0 V 5 H 15 V 1 H 11 V 0 Z M 0,0 V 5 H 1 V 1 H 5 V 0 Z"
+ id="path1" />
+ </g>
+</svg>
diff --git a/.config/ags/assets/hourglass-symbolic.svg b/.config/ags/assets/hourglass-symbolic.svg
new file mode 100644
index 0000000..aa4f97c
--- /dev/null
+++ b/.config/ags/assets/hourglass-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 5 0 c -0.96875 0 -2 1.050781 -2 2 v 2.988281 c 0 0.429688 0.222656 0.675781 0.554688 1.007813 l 2.023437 2.003906 l -2.007813 1.992188 c -0.367187 0.363281 -0.570312 0.6875 -0.570312 1 v 3.007812 c 0 1.011719 0.988281 2 2 2 h 6 c 1.007812 0 2 -1.011719 2 -2.003906 v -3.003906 c 0 -0.3125 -0.222656 -0.628907 -0.570312 -0.976563 l -2.015626 -2.015625 l 1.988282 -1.988281 c 0.261718 -0.261719 0.585937 -0.6875 0.597656 -1.015625 v -2.996094 c 0 -1.003906 -1.007812 -2 -2 -2 z m 6 4 h -6 v -2 h 6 m -3.589844 7 h 1.175782 l 2.414062 2.414062 v 1.585938 h -6 v -1.613281 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/light-mode-symbolic.svg b/.config/ags/assets/light-mode-symbolic.svg
new file mode 100644
index 0000000..d5fb271
--- /dev/null
+++ b/.config/ags/assets/light-mode-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 8 0 c -0.554688 0 -1 0.445312 -1 1 v 1 c 0 0.554688 0.445312 1 1 1 s 1 -0.445312 1 -1 v -1 c 0 -0.554688 -0.445312 -1 -1 -1 z m -4.996094 2.003906 c -0.253906 0 -0.507812 0.097656 -0.707031 0.296875 c -0.390625 0.390625 -0.390625 1.019531 0 1.414063 l 0.707031 0.707031 c 0.394532 0.390625 1.023438 0.390625 1.414063 0 c 0.394531 -0.394531 0.394531 -1.023437 0 -1.414063 l -0.707031 -0.707031 c -0.195313 -0.199219 -0.449219 -0.296875 -0.707032 -0.296875 z m 9.988282 0 c -0.253907 0 -0.507813 0.097656 -0.707032 0.296875 l -0.707031 0.707031 c -0.390625 0.390626 -0.390625 1.019532 0 1.414063 c 0.394531 0.390625 1.023437 0.390625 1.414063 0 l 0.707031 -0.707031 c 0.394531 -0.394532 0.394531 -1.023438 0 -1.414063 c -0.195313 -0.199219 -0.449219 -0.296875 -0.707031 -0.296875 z m -4.992188 1.996094 c -2.210938 0 -4 1.789062 -4 4 s 1.789062 4 4 4 s 4 -1.789062 4 -4 s -1.789062 -4 -4 -4 z m 0 2 c 1.105469 0 2 0.894531 2 2 s -0.894531 2 -2 2 s -2 -0.894531 -2 -2 s 0.894531 -2 2 -2 z m -7 1 c -0.554688 0 -1 0.445312 -1 1 s 0.445312 1 1 1 h 1 c 0.554688 0 1 -0.445312 1 -1 s -0.445312 -1 -1 -1 z m 13 0 c -0.554688 0 -1 0.445312 -1 1 s 0.445312 1 1 1 h 1 c 0.554688 0 1 -0.445312 1 -1 s -0.445312 -1 -1 -1 z m -10.335938 4.289062 c -0.238281 0.007813 -0.472656 0.105469 -0.660156 0.292969 l -0.707031 0.707031 c -0.390625 0.390626 -0.390625 1.019532 0 1.414063 c 0.394531 0.390625 1.023437 0.390625 1.414063 0 l 0.707031 -0.707031 c 0.394531 -0.394532 0.394531 -1.023438 0 -1.414063 c -0.207031 -0.210937 -0.484375 -0.308593 -0.753907 -0.292969 z m 8.574219 0 c -0.238281 0.007813 -0.472656 0.105469 -0.660156 0.292969 c -0.390625 0.390625 -0.390625 1.019531 0 1.414063 l 0.707031 0.707031 c 0.394532 0.390625 1.023438 0.390625 1.414063 0 c 0.394531 -0.394531 0.394531 -1.023437 0 -1.414063 l -0.707031 -0.707031 c -0.207032 -0.210937 -0.484376 -0.308593 -0.753907 -0.292969 z m -4.292969 1.710938 c -0.527343 0.027344 -0.945312 0.464844 -0.945312 1 v 1 c 0 0.554688 0.445312 1 1 1 s 1 -0.445312 1 -1 v -1 c 0 -0.554688 -0.445312 -1 -1 -1 c -0.015625 0 -0.035156 0 -0.050781 0 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/mixer-symbolic.svg b/.config/ags/assets/mixer-symbolic.svg
new file mode 100644
index 0000000..ad6cfa8
--- /dev/null
+++ b/.config/ags/assets/mixer-symbolic.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 11.5 1 c -1.921875 0 -3.5 1.578125 -3.5 3.5 s 1.578125 3.5 3.5 3.5 s 3.5 -1.578125 3.5 -3.5 s -1.578125 -3.5 -3.5 -3.5 z m 0 2 c 0.839844 0 1.5 0.660156 1.5 1.5 s -0.660156 1.5 -1.5 1.5 s -1.5 -0.660156 -1.5 -1.5 s 0.660156 -1.5 1.5 -1.5 z m 0 0"/>
+ <path d="m 4.5 8 c -1.921875 0 -3.5 1.578125 -3.5 3.5 s 1.578125 3.5 3.5 3.5 c 1.386719 0 2.59375 -0.820312 3.15625 -2 h 5.84375 c 0.832031 0 1.5 -0.667969 1.5 -1.5 s -0.667969 -1.5 -1.5 -1.5 h -5.84375 c -0.5625 -1.179688 -1.769531 -2 -3.15625 -2 z m 0 2 c 0.839844 0 1.5 0.660156 1.5 1.5 s -0.660156 1.5 -1.5 1.5 s -1.5 -0.660156 -1.5 -1.5 s 0.660156 -1.5 1.5 -1.5 z m 0 0"/>
+ <path d="m 2.5 3 c -0.832031 0 -1.5 0.667969 -1.5 1.5 s 0.667969 1.5 1.5 1.5 h 4.769531 c -0.175781 -0.480469 -0.265625 -0.988281 -0.269531 -1.5 c 0 -0.511719 0.09375 -1.019531 0.269531 -1.5 z m 0 0" fill-opacity="0.34902"/>
+</svg>
diff --git a/.config/ags/assets/nix-snowflake-symbolic.svg b/.config/ags/assets/nix-snowflake-symbolic.svg
new file mode 100644
index 0000000..7bb42ed
--- /dev/null
+++ b/.config/ags/assets/nix-snowflake-symbolic.svg
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="496"
+ height="496"
+ version="1"
+ id="svg6"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata
+ id="metadata12">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs10" />
+ <g
+ id="g946"
+ transform="matrix(0.97173996,0,0,0.97173996,4.043873,36.112138)">
+ <g
+ id="layer7"
+ style="display:none"
+ transform="translate(-23.75651,-24.84972)">
+ <rect
+ transform="translate(-132.5822,958.04022)"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5389"
+ width="1543.4283"
+ height="483.7439"
+ x="132.5822"
+ y="-957.77832" />
+ </g>
+ <g
+ id="layer6"
+ style="display:none"
+ transform="translate(-156.33871,933.1905)">
+ <rect
+ y="-958.02759"
+ x="132.65129"
+ height="484.30399"
+ width="550.41602"
+ id="rect5379"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5c201e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c24a46;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5372"
+ width="501.94415"
+ height="434.30405"
+ x="156.12303"
+ y="-933.02759" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d98d8a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5381"
+ width="24.939611"
+ height="24.939611"
+ x="658.02826"
+ y="-958.04022" />
+ </g>
+ <g
+ id="layer3"
+ style="display:inline;opacity:1"
+ transform="translate(37.235605,912.8581)">
+ <g
+ id="g2072"
+ transform="matrix(0.99894325,0,0,0.99894325,-36.551621,-913.90743)"
+ style="fill:#cccccc;fill-opacity:1">
+ <g
+ style="display:none;fill:#cccccc;fill-opacity:1"
+ transform="matrix(0.09048806,0,0,0.09048806,-14.15991,84.454917)"
+ id="layer1-3">
+ <rect
+ y="-2102.4253"
+ x="-1045.6049"
+ height="7145.4614"
+ width="7947.0356"
+ id="rect995"
+ style="opacity:1;fill:#cccccc;fill-opacity:1;stroke-width:10.3605" />
+ </g>
+ <g
+ transform="translate(-156.48372,537.56136)"
+ style="display:inline;opacity:1;fill:#cccccc;fill-opacity:1"
+ id="layer3-6">
+ <g
+ style="fill:#cccccc;stroke-width:11.0512;fill-opacity:1"
+ transform="matrix(0.09048806,0,0,0.09048806,142.32381,-453.10644)"
+ id="g955">
+ <g
+ transform="matrix(11.047619,0,0,11.047619,-1572.2888,9377.7107)"
+ id="g869"
+ style="fill:#cccccc;fill-opacity:1">
+ <g
+ transform="rotate(-60,226.35754,-449.37199)"
+ id="g932"
+ style="fill:#cccccc;stroke-width:11.0512;fill-opacity:1">
+ <path
+ id="path3336-6-7"
+ d="m 449.71876,-420.51322 c 40.73228,70.55837 81.46455,141.11675 122.19683,211.67512 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -9.30079,-0.004 -18.60158,-0.007 -27.90237,-0.011 -4.76362,-8.22987 -9.52724,-16.45973 -14.29086,-24.6896 15.60349,-26.83003 31.20698,-53.66007 46.81047,-80.4901 -11.07649,-19.27523 -22.15297,-38.55047 -33.22946,-57.8257 9.35083,-16.29387 18.70167,-32.58775 28.0525,-48.88162 z"
+ style="opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:33.1535;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ id="path4260-0-5"
+ d="m 309.54892,-710.38827 c 40.73228,70.55837 81.46455,141.11675 122.19683,211.67512 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -9.30079,-0.004 -18.60158,-0.007 -27.90237,-0.011 -4.76362,-8.22987 -9.52724,-16.45973 -14.29086,-24.6896 15.60349,-26.83003 31.20698,-53.66007 46.81047,-80.4901 -11.07649,-19.2752 -22.15297,-38.5504 -33.22946,-57.8256 9.35083,-16.29391 18.70167,-32.58781 28.0525,-48.88172 z"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:33.1535;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <use
+ x="0"
+ y="0"
+ xlink:href="#path3336-6-7"
+ id="use3439-6-3"
+ transform="rotate(60,728.23563,-692.24036)"
+ width="100%"
+ height="100%"
+ style="fill:#cccccc;fill-opacity:1;stroke-width:11.0512" />
+ <use
+ x="0"
+ y="0"
+ xlink:href="#path3336-6-7"
+ id="use3449-5-5"
+ transform="rotate(180,477.5036,-570.81898)"
+ width="100%"
+ height="100%"
+ style="fill:#cccccc;fill-opacity:1;stroke-width:11.0512" />
+ <use
+ style="display:inline;fill:#cccccc;fill-opacity:1;stroke-width:11.0512"
+ x="0"
+ y="0"
+ xlink:href="#path4260-0-5"
+ id="use4354-5-6"
+ transform="rotate(120,407.33916,-716.08356)"
+ width="100%"
+ height="100%" />
+ <use
+ style="display:inline;fill:#cccccc;fill-opacity:1;stroke-width:11.0512"
+ x="0"
+ y="0"
+ xlink:href="#path4260-0-5"
+ id="use4362-2-2"
+ transform="rotate(-120,407.28823,-715.86995)"
+ width="100%"
+ height="100%" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/.config/ags/assets/nixos-symbolic.svg b/.config/ags/assets/nixos-symbolic.svg
new file mode 100644
index 0000000..7bb42ed
--- /dev/null
+++ b/.config/ags/assets/nixos-symbolic.svg
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="496"
+ height="496"
+ version="1"
+ id="svg6"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata
+ id="metadata12">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs10" />
+ <g
+ id="g946"
+ transform="matrix(0.97173996,0,0,0.97173996,4.043873,36.112138)">
+ <g
+ id="layer7"
+ style="display:none"
+ transform="translate(-23.75651,-24.84972)">
+ <rect
+ transform="translate(-132.5822,958.04022)"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5389"
+ width="1543.4283"
+ height="483.7439"
+ x="132.5822"
+ y="-957.77832" />
+ </g>
+ <g
+ id="layer6"
+ style="display:none"
+ transform="translate(-156.33871,933.1905)">
+ <rect
+ y="-958.02759"
+ x="132.65129"
+ height="484.30399"
+ width="550.41602"
+ id="rect5379"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5c201e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c24a46;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5372"
+ width="501.94415"
+ height="434.30405"
+ x="156.12303"
+ y="-933.02759" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d98d8a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5381"
+ width="24.939611"
+ height="24.939611"
+ x="658.02826"
+ y="-958.04022" />
+ </g>
+ <g
+ id="layer3"
+ style="display:inline;opacity:1"
+ transform="translate(37.235605,912.8581)">
+ <g
+ id="g2072"
+ transform="matrix(0.99894325,0,0,0.99894325,-36.551621,-913.90743)"
+ style="fill:#cccccc;fill-opacity:1">
+ <g
+ style="display:none;fill:#cccccc;fill-opacity:1"
+ transform="matrix(0.09048806,0,0,0.09048806,-14.15991,84.454917)"
+ id="layer1-3">
+ <rect
+ y="-2102.4253"
+ x="-1045.6049"
+ height="7145.4614"
+ width="7947.0356"
+ id="rect995"
+ style="opacity:1;fill:#cccccc;fill-opacity:1;stroke-width:10.3605" />
+ </g>
+ <g
+ transform="translate(-156.48372,537.56136)"
+ style="display:inline;opacity:1;fill:#cccccc;fill-opacity:1"
+ id="layer3-6">
+ <g
+ style="fill:#cccccc;stroke-width:11.0512;fill-opacity:1"
+ transform="matrix(0.09048806,0,0,0.09048806,142.32381,-453.10644)"
+ id="g955">
+ <g
+ transform="matrix(11.047619,0,0,11.047619,-1572.2888,9377.7107)"
+ id="g869"
+ style="fill:#cccccc;fill-opacity:1">
+ <g
+ transform="rotate(-60,226.35754,-449.37199)"
+ id="g932"
+ style="fill:#cccccc;stroke-width:11.0512;fill-opacity:1">
+ <path
+ id="path3336-6-7"
+ d="m 449.71876,-420.51322 c 40.73228,70.55837 81.46455,141.11675 122.19683,211.67512 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -9.30079,-0.004 -18.60158,-0.007 -27.90237,-0.011 -4.76362,-8.22987 -9.52724,-16.45973 -14.29086,-24.6896 15.60349,-26.83003 31.20698,-53.66007 46.81047,-80.4901 -11.07649,-19.27523 -22.15297,-38.55047 -33.22946,-57.8257 9.35083,-16.29387 18.70167,-32.58775 28.0525,-48.88162 z"
+ style="opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:33.1535;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ </g>
+ <path
+ id="path4260-0-5"
+ d="m 309.54892,-710.38827 c 40.73228,70.55837 81.46455,141.11675 122.19683,211.67512 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -9.30079,-0.004 -18.60158,-0.007 -27.90237,-0.011 -4.76362,-8.22987 -9.52724,-16.45973 -14.29086,-24.6896 15.60349,-26.83003 31.20698,-53.66007 46.81047,-80.4901 -11.07649,-19.2752 -22.15297,-38.5504 -33.22946,-57.8256 9.35083,-16.29391 18.70167,-32.58781 28.0525,-48.88172 z"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#cccccc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:33.1535;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <use
+ x="0"
+ y="0"
+ xlink:href="#path3336-6-7"
+ id="use3439-6-3"
+ transform="rotate(60,728.23563,-692.24036)"
+ width="100%"
+ height="100%"
+ style="fill:#cccccc;fill-opacity:1;stroke-width:11.0512" />
+ <use
+ x="0"
+ y="0"
+ xlink:href="#path3336-6-7"
+ id="use3449-5-5"
+ transform="rotate(180,477.5036,-570.81898)"
+ width="100%"
+ height="100%"
+ style="fill:#cccccc;fill-opacity:1;stroke-width:11.0512" />
+ <use
+ style="display:inline;fill:#cccccc;fill-opacity:1;stroke-width:11.0512"
+ x="0"
+ y="0"
+ xlink:href="#path4260-0-5"
+ id="use4354-5-6"
+ transform="rotate(120,407.33916,-716.08356)"
+ width="100%"
+ height="100%" />
+ <use
+ style="display:inline;fill:#cccccc;fill-opacity:1;stroke-width:11.0512"
+ x="0"
+ y="0"
+ xlink:href="#path4260-0-5"
+ id="use4362-2-2"
+ transform="rotate(-120,407.28823,-715.86995)"
+ width="100%"
+ height="100%" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/.config/ags/assets/nixos.svg b/.config/ags/assets/nixos.svg
new file mode 100644
index 0000000..1a756ed
--- /dev/null
+++ b/.config/ags/assets/nixos.svg
@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="496"
+ height="496"
+ version="1"
+ id="svg6"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:dc="http://purl.org/dc/elements/1.1/">
+ <metadata
+ id="metadata12">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs10">
+ <linearGradient
+ id="linearGradient5562">
+ <stop
+ style="stop-color:#699ad7;stop-opacity:1"
+ offset="0"
+ id="stop5564" />
+ <stop
+ id="stop5566"
+ offset="0.24345198"
+ style="stop-color:#7eb1dd;stop-opacity:1" />
+ <stop
+ style="stop-color:#7ebae4;stop-opacity:1"
+ offset="1"
+ id="stop5568" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5053">
+ <stop
+ style="stop-color:#415e9a;stop-opacity:1"
+ offset="0"
+ id="stop5055" />
+ <stop
+ id="stop5057"
+ offset="0.23168644"
+ style="stop-color:#4a6baf;stop-opacity:1" />
+ <stop
+ style="stop-color:#5277c3;stop-opacity:1"
+ offset="1"
+ id="stop5059" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient5960">
+ <stop
+ id="stop5962"
+ offset="0"
+ style="stop-color:#637ddf;stop-opacity:1" />
+ <stop
+ style="stop-color:#649afa;stop-opacity:1"
+ offset="0.23168644"
+ id="stop5964" />
+ <stop
+ id="stop5966"
+ offset="1"
+ style="stop-color:#719efa;stop-opacity:1" />
+ </linearGradient>
+ <linearGradient
+ y2="515.97058"
+ x2="282.26105"
+ y1="338.62445"
+ x1="213.95642"
+ gradientTransform="translate(983.36076,293.12113)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient5855"
+ xlink:href="#linearGradient5960" />
+ <linearGradient
+ xlink:href="#linearGradient5562"
+ id="linearGradient4328"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(70.650339,-1055.1511)"
+ x1="200.59668"
+ y1="351.41116"
+ x2="290.08701"
+ y2="506.18814" />
+ <linearGradient
+ xlink:href="#linearGradient5053"
+ id="linearGradient4330"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(864.69589,-1491.3405)"
+ x1="-584.19934"
+ y1="782.33563"
+ x2="-496.29703"
+ y2="937.71399" />
+ </defs>
+ <g
+ id="g946"
+ transform="matrix(0.97173996,0,0,0.97173996,4.043873,36.112138)">
+ <g
+ id="layer7"
+ style="display:none"
+ transform="translate(-23.75651,-24.84972)">
+ <rect
+ transform="translate(-132.5822,958.04022)"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5389"
+ width="1543.4283"
+ height="483.7439"
+ x="132.5822"
+ y="-957.77832" />
+ </g>
+ <g
+ id="layer6"
+ style="display:none"
+ transform="translate(-156.33871,933.1905)">
+ <rect
+ y="-958.02759"
+ x="132.65129"
+ height="484.30399"
+ width="550.41602"
+ id="rect5379"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5c201e;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c24a46;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5372"
+ width="501.94415"
+ height="434.30405"
+ x="156.12303"
+ y="-933.02759" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#d98d8a;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5381"
+ width="24.939611"
+ height="24.939611"
+ x="658.02826"
+ y="-958.04022" />
+ </g>
+ <g
+ id="layer1"
+ style="display:inline"
+ transform="translate(-156.33871,933.1905)">
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5277c3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 309.40365,-710.2521 c 40.73228,70.55837 81.46455,141.11673 122.19683,211.6751 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -9.30079,-0.004 -18.60158,-0.007 -27.90237,-0.011 -4.76362,-8.22987 -9.52724,-16.45973 -14.29086,-24.6896 15.60349,-26.83007 31.20698,-53.66013 46.81047,-80.4902 -11.07649,-19.2752 -22.15297,-38.5504 -33.22946,-57.8256 9.35083,-16.29387 18.70167,-32.58773 28.0525,-48.8816 z"
+ id="path4861" />
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#7ebae4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 353.50926,-797.4433 c -40.73919,70.55437 -81.47837,141.10873 -122.21756,211.6631 -9.51159,-16.12333 -19.02318,-32.24667 -28.53477,-48.37 10.97946,-18.89583 21.95893,-37.79167 32.93839,-56.6875 -21.80507,-0.0573 -43.61014,-0.1146 -65.41521,-0.1719 -4.64713,-8.0566 -9.29427,-16.1132 -13.9414,-24.1698 4.74546,-8.24033 9.49091,-16.48067 14.23637,-24.721 31.03726,0.098 62.07451,0.19593 93.11177,0.2939 11.15457,-19.2301 22.30914,-38.4602 33.46371,-57.6903 18.78623,-0.0488 37.57247,-0.0977 56.3587,-0.1465 z"
+ id="use4863" />
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#7ebae4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 362.88537,-628.243 c 81.47146,0.004 162.94293,0.008 244.41439,0.012 -9.20743,16.29893 -18.41486,32.59787 -27.62229,48.8968 -21.854,-0.0606 -43.70799,-0.12113 -65.56199,-0.1817 10.85292,18.91237 21.70584,37.82473 32.55876,56.7371 -4.65366,8.05283 -9.30732,16.10567 -13.96098,24.1585 -9.50907,0.0107 -19.01815,0.0213 -28.52722,0.032 -15.43377,-26.92803 -30.86753,-53.85607 -46.3013,-80.7841 -22.23106,-0.0451 -44.46211,-0.0902 -66.69317,-0.1353 -9.4354,-16.2451 -18.8708,-32.4902 -28.3062,-48.7353 z"
+ id="use4865" />
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#7ebae4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 505.14318,-720.9886 c -40.73228,-70.55837 -81.46455,-141.11673 -122.19683,-211.6751 18.71902,-0.1756 37.43804,-0.3512 56.15706,-0.5268 10.87453,18.9564 21.74907,37.9128 32.6236,56.8692 10.95215,-18.8551 21.9043,-37.7102 32.85645,-56.5653 9.30079,0.004 18.60158,0.007 27.90237,0.011 4.76362,8.22987 9.52724,16.45973 14.29086,24.6896 -15.60349,26.83007 -31.20698,53.66013 -46.81047,80.4902 11.07649,19.2752 22.15297,38.5504 33.22946,57.8256 -9.35083,16.29387 -18.70167,32.58773 -28.0525,48.8816 z"
+ id="use4867" />
+ <path
+ id="path4873"
+ d="m 309.40365,-710.2521 c 40.73228,70.55837 81.46455,141.11673 122.19683,211.6751 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -9.30079,-0.004 -18.60158,-0.007 -27.90237,-0.011 -4.76362,-8.22987 -9.52724,-16.45973 -14.29086,-24.6896 15.60349,-26.83007 31.20698,-53.66013 46.81047,-80.4902 -11.07649,-19.2752 -22.15297,-38.5504 -33.22946,-57.8256 9.35083,-16.29387 18.70167,-32.58773 28.0525,-48.8816 z"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5277c3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <path
+ id="use4875"
+ d="m 451.3364,-803.53264 c -81.47147,-0.004 -162.94293,-0.008 -244.4144,-0.012 9.20743,-16.29895 18.41486,-32.5979 27.62229,-48.89685 21.854,0.0606 43.70799,0.12117 65.56199,0.18175 -10.85292,-18.91239 -21.70583,-37.82478 -32.55875,-56.73717 4.65366,-8.05284 9.30731,-16.10567 13.96097,-24.15851 9.50907,-0.0105 19.01815,-0.021 28.52722,-0.0315 15.43377,26.92805 30.86753,53.85609 46.3013,80.78414 22.23106,0.0451 44.46211,0.0902 66.69317,0.13524 9.4354,16.24497 18.87081,32.48993 28.30621,48.7349 z"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5277c3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <path
+ id="use4877"
+ d="m 460.87178,-633.8425 c 40.73919,-70.55435 81.47838,-141.10869 122.21757,-211.66304 9.51159,16.12334 19.02318,32.24669 28.53477,48.37003 -10.97946,18.89584 -21.95893,37.79167 -32.93839,56.68751 21.80507,0.0573 43.61013,0.11453 65.4152,0.1718 4.64713,8.0566 9.29427,16.1132 13.9414,24.1698 -4.74545,8.24037 -9.49091,16.48073 -14.23636,24.7211 -31.03726,-0.098 -62.07451,-0.196 -93.11177,-0.294 -11.15457,19.23013 -22.30914,38.46027 -33.46371,57.6904 -18.78624,0.0488 -37.57247,0.0976 -56.35871,0.1464 z"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#5277c3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <g
+ id="layer2"
+ style="display:none"
+ transform="translate(72.039038,-1799.4476)">
+ <path
+ d="M 460.60629,594.72881 209.74183,594.7288 84.309616,377.4738 209.74185,160.21882 l 250.86446,1e-5 125.43222,217.255 z"
+ id="path6032"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:0.236;fill:#4e4d52;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <path
+ transform="translate(0,-308.26772)"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#4e4d52;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="path5875"
+ d="m 385.59154,773.06721 -100.83495,0 -50.41747,-87.32564 50.41748,-87.32563 100.83495,10e-6 50.41748,87.32563 z" />
+ <path
+ id="path5851"
+ d="m 1216.5591,630.26623 c 41.0182,76.04675 82.0363,152.09355 123.0545,228.14035 -14.2269,-0.4205 -28.4538,-0.8411 -42.6807,-1.2616 -14.4941,-26.5908 -28.9882,-53.1817 -43.4823,-79.7725 -13.2169,26.7756 -26.4337,53.5511 -39.6506,80.3267 -10.8958,-6.5995 -21.7917,-13.1989 -32.6875,-19.7984 17.8246,-33.4283 35.6491,-66.8565 53.4737,-100.2848 -12.3719,-24.6298 -24.7438,-49.2597 -37.1157,-73.88955 6.3629,-11.1534 12.7257,-22.3068 19.0886,-33.4602 z"
+ style="fill:url(#linearGradient5855);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.415;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#c53a3a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect5884"
+ width="48.834862"
+ height="226.22897"
+ x="-34.74221"
+ y="446.17056"
+ transform="rotate(-30)" />
+ <path
+ transform="translate(0,-308.26772)"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.509;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="path3428"
+ d="m 251.98568,878.63831 -14.02447,24.29109 h -28.04894 l -14.02447,-24.29109 14.02447,-24.2911 h 28.04894 z" />
+ <use
+ x="0"
+ y="0"
+ xlink:href="#rect5884"
+ id="use4252"
+ transform="rotate(60,268.29786,489.4515)"
+ width="100%"
+ height="100%" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:0.650794;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect4254"
+ width="5.3947482"
+ height="115.12564"
+ x="545.71014"
+ y="467.07007"
+ transform="rotate(30,575.23539,-154.13386)" />
+ </g>
+ </g>
+ <g
+ id="layer3"
+ style="display:inline;opacity:1"
+ transform="translate(-156.33871,933.1905)">
+ <path
+ id="path3336-6"
+ d="m 309.54892,-710.38827 c 40.73228,70.55837 81.46455,141.11675 122.19683,211.67512 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -9.30079,-0.004 -18.60158,-0.007 -27.90237,-0.011 -4.76362,-8.22987 -9.52724,-16.45973 -14.29086,-24.6896 15.60349,-26.83003 31.20698,-53.66007 46.81047,-80.4901 -11.07649,-19.27523 -22.15297,-38.55047 -33.22946,-57.8257 9.35083,-16.29387 18.70167,-32.58775 28.0525,-48.88162 z"
+ style="opacity:1;fill:url(#linearGradient4328);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ <use
+ height="100%"
+ width="100%"
+ transform="rotate(60,407.11155,-715.78724)"
+ id="use3439-6"
+ xlink:href="#path3336-6"
+ y="0"
+ x="0" />
+ <use
+ height="100%"
+ width="100%"
+ transform="rotate(-60,407.31177,-715.70016)"
+ id="use3445-0"
+ xlink:href="#path3336-6"
+ y="0"
+ x="0" />
+ <use
+ height="100%"
+ width="100%"
+ transform="rotate(180,407.41868,-715.7565)"
+ id="use3449-5"
+ xlink:href="#path3336-6"
+ y="0"
+ x="0" />
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient4330);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:3;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m 309.54892,-710.38827 c 40.73228,70.55837 81.46455,141.11675 122.19683,211.67512 -18.71902,0.1756 -37.43804,0.3512 -56.15706,0.5268 -10.87453,-18.9564 -21.74907,-37.9128 -32.6236,-56.8692 -10.95215,18.8551 -21.9043,37.7102 -32.85645,56.5653 -10.54113,-2.26829 -26.58606,6.01638 -31.9377,-7.5219 -4.39393,-6.91787 -12.57856,-15.53043 -6.85074,-23.97221 8.26178,-12.05394 14.90093,-25.28023 22.52611,-37.79439 6.95986,-11.9674 13.91971,-23.9348 20.87957,-35.9022 -11.07649,-19.2752 -22.15297,-38.5504 -33.22946,-57.8256 9.35083,-16.29391 18.70167,-32.58781 28.0525,-48.88172 z"
+ id="path4260-0" />
+ <use
+ height="100%"
+ width="100%"
+ transform="rotate(120,407.33916,-716.08356)"
+ id="use4354-5"
+ xlink:href="#path4260-0"
+ y="0"
+ x="0"
+ style="display:inline" />
+ <use
+ height="100%"
+ width="100%"
+ transform="rotate(-120,407.28823,-715.86995)"
+ id="use4362-2"
+ xlink:href="#path4260-0"
+ y="0"
+ x="0"
+ style="display:inline" />
+ </g>
+ </g>
+</svg>
diff --git a/.config/ags/assets/osk.svg b/.config/ags/assets/osk.svg
new file mode 100644
index 0000000..511420a
--- /dev/null
+++ b/.config/ags/assets/osk.svg
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="24"
+ height="24"
+ version="1"
+ id="svg20"
+ sodipodi:docname="osk.svg"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs20" />
+ <sodipodi:namedview
+ id="namedview20"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="26.166667"
+ inkscape:cx="12"
+ inkscape:cy="12"
+ inkscape:current-layer="svg20" />
+ <rect
+ style="fill:#4f4f4f;stroke-width:0.871697"
+ width="11.938859"
+ height="17.820761"
+ x="-18.006235"
+ y="-20.910379"
+ rx="0.85277563"
+ ry="0.891038"
+ transform="matrix(0,-1,-1,0,0,0)"
+ id="rect1" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="m 9.3268859,13.31597 a 0.89103802,0.85277564 0 0 1 -0.891038,0.852775 0.89103802,0.85277564 0 0 1 -0.891038,-0.852775 0.89103802,0.85277564 0 0 1 0.891038,-0.852776 0.89103802,0.85277564 0 0 1 0.891038,0.852776 z"
+ id="path1" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="M 12,13.31597 A 0.89103802,0.85277564 0 0 1 11.108962,14.168745 0.89103802,0.85277564 0 0 1 10.217924,13.31597 0.89103802,0.85277564 0 0 1 11.108962,12.463194 0.89103802,0.85277564 0 0 1 12,13.31597 Z"
+ id="path2" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="m 14.673114,13.31597 a 0.89103802,0.85277564 0 0 1 -0.891038,0.852775 0.89103802,0.85277564 0 0 1 -0.891038,-0.852775 0.89103802,0.85277564 0 0 1 0.891038,-0.852776 0.89103802,0.85277564 0 0 1 0.891038,0.852776 z"
+ id="path3" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="M 17.346228,13.31597 A 0.89103802,0.85277564 0 0 1 16.45519,14.168745 0.89103802,0.85277564 0 0 1 15.564152,13.31597 0.89103802,0.85277564 0 0 1 16.45519,12.463194 0.89103802,0.85277564 0 0 1 17.346228,13.31597 Z"
+ id="path4" />
+ <rect
+ style="opacity:0.2;stroke-width:0.871697"
+ width="12.474532"
+ height="1.7055513"
+ x="5.7627339"
+ y="15.874296"
+ rx="0.41611475"
+ ry="0.42638782"
+ id="rect4" />
+ <rect
+ style="fill:#e4e4e4;stroke-width:0.871697"
+ width="12.474532"
+ height="1.7055513"
+ x="5.7627339"
+ y="15.447908"
+ rx="0.41611475"
+ ry="0.42638782"
+ id="rect5" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="M 15.564152,9.904867 A 0.89103802,0.85277564 0 0 1 14.673114,10.757643 0.89103802,0.85277564 0 0 1 13.782076,9.904867 0.89103802,0.85277564 0 0 1 14.673114,9.0520913 0.89103802,0.85277564 0 0 1 15.564152,9.904867 Z"
+ id="path5" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="M 7.5448099,9.904867 A 0.89103802,0.85277564 0 0 1 6.6537719,10.757643 0.89103802,0.85277564 0 0 1 5.7627339,9.904867 0.89103802,0.85277564 0 0 1 6.6537719,9.0520913 0.89103802,0.85277564 0 0 1 7.5448099,9.904867 Z"
+ id="path6" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="M 10.217924,9.904867 A 0.89103802,0.85277564 0 0 1 9.3268859,10.757643 0.89103802,0.85277564 0 0 1 8.4358479,9.904867 0.89103802,0.85277564 0 0 1 9.3268859,9.0520913 0.89103802,0.85277564 0 0 1 10.217924,9.904867 Z"
+ id="path7" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="M 12.891038,9.904867 A 0.89103802,0.85277564 0 0 1 12,10.757643 0.89103802,0.85277564 0 0 1 11.108962,9.904867 0.89103802,0.85277564 0 0 1 12,9.0520913 0.89103802,0.85277564 0 0 1 12.891038,9.904867 Z"
+ id="path8" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="M 18.237266,9.904867 A 0.89103802,0.85277564 0 0 1 17.346228,10.757643 0.89103802,0.85277564 0 0 1 16.45519,9.904867 0.89103802,0.85277564 0 0 1 17.346228,9.0520913 0.89103802,0.85277564 0 0 1 18.237266,9.904867 Z"
+ id="path9" />
+ <g
+ style="fill:#e4e4e4"
+ id="g18"
+ transform="matrix(0.89103802,0,0,0.85277564,1.3075438,1.8034984)">
+ <path
+ d="M 9,13 A 1,1 0 0 1 8,14 1,1 0 0 1 7,13 1,1 0 0 1 8,12 1,1 0 0 1 9,13 Z"
+ id="path10" />
+ <path
+ d="m 12,13 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z"
+ id="path11" />
+ <path
+ d="m 15,13 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z"
+ id="path12" />
+ <path
+ d="m 18,13 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z"
+ id="path13" />
+ <path
+ d="m 16,9 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z"
+ id="path14" />
+ <path
+ d="M 7,9 A 1,1 0 0 1 6,10 1,1 0 0 1 5,9 1,1 0 0 1 6,8 1,1 0 0 1 7,9 Z"
+ id="path15" />
+ <path
+ d="M 10,9 A 1,1 0 0 1 9,10 1,1 0 0 1 8,9 1,1 0 0 1 9,8 1,1 0 0 1 10,9 Z"
+ id="path16" />
+ <path
+ d="m 13,9 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z"
+ id="path17" />
+ <path
+ d="m 19,9 a 1,1 0 0 1 -1,1 1,1 0 0 1 -1,-1 1,1 0 0 1 1,-1 1,1 0 0 1 1,1 z"
+ id="path18" />
+ </g>
+ <path
+ style="opacity:0.1;fill:#ffffff;stroke-width:0.871697"
+ d="m 3.9806578,6.0673766 c -0.493635,0 -0.891038,0.3803379 -0.891038,0.8527756 v 0.4263879 c 0,-0.4724377 0.397403,-0.8527757 0.891038,-0.8527757 H 20.019342 c 0.493635,0 0.891038,0.380338 0.891038,0.8527757 V 6.9201522 c 0,-0.4724377 -0.397403,-0.8527756 -0.891038,-0.8527756 z"
+ id="path19" />
+ <path
+ style="opacity:0.2;stroke-width:0.871697"
+ d="m 3.0896198,17.15346 v 0.426388 c 0,0.472437 0.397403,0.852775 0.891038,0.852775 H 20.019342 c 0.493635,0 0.891038,-0.380338 0.891038,-0.852775 V 17.15346 c 0,0.472438 -0.397403,0.852776 -0.891038,0.852776 H 3.9806578 c -0.493635,0 -0.891038,-0.380338 -0.891038,-0.852776 z"
+ id="path20" />
+</svg>
diff --git a/.config/ags/assets/pinned.svg b/.config/ags/assets/pinned.svg
new file mode 100644
index 0000000..ce8efb3
--- /dev/null
+++ b/.config/ags/assets/pinned.svg
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ viewBox="0 0 22 22"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="pinned.svg"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="21.454545"
+ inkscape:cx="11"
+ inkscape:cy="11"
+ inkscape:current-layer="svg1" />
+ <defs
+ id="defs3051">
+ <style
+ type="text/css"
+ id="current-color-scheme">.ColorScheme-Text { color: #fcfcfc; } </style>
+ </defs>
+ <path
+ style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:0.906415"
+ d="m 11,3.7453986 c -4.015418,0 -7.2480469,3.2355522 -7.2480469,7.2546014 0,4.019049 3.2326289,7.254601 7.2480469,7.254601 4.015418,0 7.248047,-3.235552 7.248047,-7.254601 0,-4.0190492 -3.232629,-7.2546014 -7.248047,-7.2546014 z m 0,0.9068251 c 3.513491,0 6.342041,2.8311083 6.342041,6.3477763 0,3.516668 -2.82855,6.347776 -6.342041,6.347776 C 7.4865093,17.347776 4.657959,14.516668 4.657959,11 4.657959,7.483332 7.4865093,4.6522237 11,4.6522237 Z m 0,4.5341259 c -1.0038533,0 -1.8120117,0.8088893 -1.8120117,1.8136504 0,1.004761 0.8081584,1.81365 1.8120117,1.81365 1.003854,0 1.812012,-0.808889 1.812012,-1.81365 0,-1.0047611 -0.808158,-1.8136504 -1.812012,-1.8136504 z m 0,0.9068254 c 0.501926,0 0.906006,0.404445 0.906006,0.906825 0,0.50238 -0.40408,0.906825 -0.906006,0.906825 -0.501926,0 -0.906006,-0.404445 -0.906006,-0.906825 0,-0.50238 0.40408,-0.906825 0.906006,-0.906825 z"
+ class="ColorScheme-Text"
+ id="path1" />
+</svg>
diff --git a/.config/ags/assets/preferences-desktop-theme-symbolic.svg b/.config/ags/assets/preferences-desktop-theme-symbolic.svg
new file mode 100644
index 0000000..4461454
--- /dev/null
+++ b/.config/ags/assets/preferences-desktop-theme-symbolic.svg
@@ -0,0 +1,321 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ height="16px"
+ viewBox="0 0 16 16"
+ width="16px"
+ version="1.1"
+ id="svg3533"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs3537" />
+ <filter
+ id="a"
+ height="1"
+ width="1"
+ x="0"
+ y="0">
+ <feColorMatrix
+ in="SourceGraphic"
+ type="matrix"
+ values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"
+ id="feColorMatrix3414" />
+ </filter>
+ <mask
+ id="b">
+ <g
+ filter="url(#a)"
+ id="g3419">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwcB8GZcBWQFEIoq/qy3b9Y8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQdi/bMl/PXorsAAADAA+ffDwAAAAD7CQAAAAAQIAAAAABAgAAAAAAAAQIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8dQMXagEYjYWofQAAAABJRU5ErkJggg=="
+ id="image3417" />
+ </g>
+ </mask>
+ <clipPath
+ id="c">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3422" />
+ </clipPath>
+ <mask
+ id="d">
+ <g
+ filter="url(#a)"
+ id="g3427">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwUCd/SdlAFYAkYji72rLdv1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBx6I98+X8teguAAAA8MD59wMAAADAfgIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAECAAAAAAAABAgAAAAAECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX90LagEYFVKkEwAAAABJRU5ErkJggg=="
+ id="image3425" />
+ </g>
+ </mask>
+ <clipPath
+ id="e">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3430" />
+ </clipPath>
+ <mask
+ id="f">
+ <g
+ filter="url(#a)"
+ id="g3435">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwUCd/SdlAFYAkYji72rLdv1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBx6I98+X8teguAAAA8MD59wMAAADAfgIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAECAAAAAAAABAgAAAAAECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX90LagEYFVKkEwAAAABJRU5ErkJggg=="
+ id="image3433" />
+ </g>
+ </mask>
+ <clipPath
+ id="g">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3438" />
+ </clipPath>
+ <mask
+ id="h">
+ <g
+ filter="url(#a)"
+ id="g3443">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwUCd/SdlAFYAkYji72rLdv1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBx6I98+X8teguAAAA8MD59wMAAADAfgIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAECAAAAAAAABAgAAAAAECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX90LagEYFVKkEwAAAABJRU5ErkJggg=="
+ id="image3441" />
+ </g>
+ </mask>
+ <clipPath
+ id="i">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3446" />
+ </clipPath>
+ <mask
+ id="j">
+ <g
+ filter="url(#a)"
+ id="g3451">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwUCd/SdlAFYAkYji72rLdv1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBx6I98+X8teguAAAA8MD59wMAAADAfgIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAECAAAAAAAABAgAAAAAECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX90LagEYFVKkEwAAAABJRU5ErkJggg=="
+ id="image3449" />
+ </g>
+ </mask>
+ <clipPath
+ id="k">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3454" />
+ </clipPath>
+ <mask
+ id="l">
+ <g
+ filter="url(#a)"
+ id="g3459">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwUCd/SdlAFYAkYji72rLdv1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBx6I98+X8teguAAAA8MD59wMAAADAfgIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAECAAAAAAAABAgAAAAAECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX90LagEYFVKkEwAAAABJRU5ErkJggg=="
+ id="image3457" />
+ </g>
+ </mask>
+ <clipPath
+ id="m">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3462" />
+ </clipPath>
+ <mask
+ id="n">
+ <g
+ filter="url(#a)"
+ id="g3467">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwUCd/SdlAFYAkYji72rLdv1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBx6I98+X8teguAAAA8MD59wMAAADAfgIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAECAAAAAAAABAgAAAAAECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAX90LagEYFVKkEwAAAABJRU5ErkJggg=="
+ id="image3465" />
+ </g>
+ </mask>
+ <clipPath
+ id="o">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3470" />
+ </clipPath>
+ <mask
+ id="p">
+ <g
+ filter="url(#a)"
+ id="g3475">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMr0lEQVR4nO3YsQ2AMAwAwcB8GZcBWQFEIoq/qy3b9Y8BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQdi/bMl/PXorsAAADAA+ffDwAAAAD7CQAAAAAQIAAAAABAgAAAAAAAAQIAAAAABAgAAAAAECAAAAAAQIAAAAAAAAECAAAAAAQIAAAAABAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8dQMXagEYjYWofQAAAABJRU5ErkJggg=="
+ id="image3473" />
+ </g>
+ </mask>
+ <clipPath
+ id="q">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3478" />
+ </clipPath>
+ <mask
+ id="r">
+ <g
+ filter="url(#a)"
+ id="g3483">
+ <image
+ height="800"
+ width="1024"
+ xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMgCAYAAAC9FhA7AAAABmJLR0QA/wD/AP+gvaeTAAAMqUlEQVR4nO3YsQ3AMAwEMSeTe/QM4QgujqwF4etbCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIen76s4fvAQAAgAPv7QEAAADAPAEAAAAAAgQAAAAACBAAAAAAIEAAAAAAgAABAAAAAAIEAAAAAAgQAAAAACBAAAAAAIAAAQAAAAACBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAUx8hKgEYqUBSPgAAAABJRU5ErkJggg=="
+ id="image3481" />
+ </g>
+ </mask>
+ <clipPath
+ id="s">
+ <path
+ d="m 0 0 h 1024 v 800 h -1024 z"
+ id="path3486" />
+ </clipPath>
+ <path
+ d="m 3 1 c -1.644531 0 -3 1.355469 -3 3 v 6 c 0 1.644531 1.355469 3 3 3 h 10 c 1.644531 0 3 -1.355469 3 -3 v -6 c 0 -0.570312 -0.167969 -1.101562 -0.449219 -1.558594 l -1.550781 1.554688 v 6.003906 c 0 0.570312 -0.429688 1 -1 1 h -10 c -0.570312 0 -1 -0.429688 -1 -1 v -6 c 0 -0.570312 0.429688 -1 1 -1 h 5.96875 l 2.007812 -2 z m 0 0"
+ fill="#2e3436"
+ id="path3489"
+ style="fill:#000000" />
+ <g
+ clip-path="url(#c)"
+ mask="url(#b)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3493"
+ style="fill:#000000">
+ <path
+ d="m 439.105469 225.78125 h 7.839843 c -0.890624 0.371094 -0.972656 1.847656 0 2.25 h -7.839843 z m 0 0"
+ fill="#2e3436"
+ id="path3491"
+ style="fill:#000000" />
+ </g>
+ <g
+ clip-path="url(#e)"
+ mask="url(#d)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3497"
+ style="fill:#000000">
+ <path
+ d="m 29.25 627.75 h 0.75 v 0.75 h -0.75 z m 0 0"
+ fill="#2e3436"
+ fill-rule="evenodd"
+ id="path3495"
+ style="fill:#000000" />
+ </g>
+ <g
+ clip-path="url(#g)"
+ mask="url(#f)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3501"
+ style="fill:#000000">
+ <path
+ d="m 30 627 h 0.75 v 0.75 h -0.75 z m 0 0"
+ fill="#2e3436"
+ fill-rule="evenodd"
+ id="path3499"
+ style="fill:#000000" />
+ </g>
+ <g
+ clip-path="url(#i)"
+ mask="url(#h)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3505"
+ style="fill:#000000">
+ <path
+ d="m 30.75 629.25 h 0.75 v 0.75 h -0.75 z m 0 0"
+ fill="#2e3436"
+ fill-rule="evenodd"
+ id="path3503"
+ style="fill:#000000" />
+ </g>
+ <g
+ clip-path="url(#k)"
+ mask="url(#j)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3509"
+ style="fill:#000000">
+ <path
+ d="m 29.25 629.25 h 0.75 v 0.75 h -0.75 z m 0 0"
+ fill="#2e3436"
+ fill-rule="evenodd"
+ id="path3507"
+ style="fill:#000000" />
+ </g>
+ <g
+ clip-path="url(#m)"
+ mask="url(#l)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3513"
+ style="fill:#000000">
+ <path
+ d="m 30 630 h 0.75 v 0.75 h -0.75 z m 0 0"
+ fill="#2e3436"
+ fill-rule="evenodd"
+ id="path3511"
+ style="fill:#000000" />
+ </g>
+ <g
+ clip-path="url(#o)"
+ mask="url(#n)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3517"
+ style="fill:#000000">
+ <path
+ d="m 31.5 630 h 0.75 v 0.75 h -0.75 z m 0 0"
+ fill="#2e3436"
+ fill-rule="evenodd"
+ id="path3515"
+ style="fill:#000000" />
+ </g>
+ <g
+ clip-path="url(#q)"
+ mask="url(#p)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3521"
+ style="fill:#000000">
+ <path
+ d="m 119.253906 648.75 v 5.25 h 5.25 v -5.25 z m 0 0"
+ fill="#2e3436"
+ id="path3519"
+ style="fill:#000000" />
+ </g>
+ <path
+ d="m 11 7 c 0 1.65625 -1.339844 3.007812 -3 3 h -3 v -3 c 0 -1.660156 1.34375 -3 3 -3 c 1.660156 0 3 1.339844 3 3 z m 0 0"
+ fill="#2e3436"
+ id="path3523"
+ style="fill:#000000" />
+ <path
+ d="m 13.398438 0 l -3.46875 3.457031 c 0.683593 0.355469 1.234374 0.910157 1.589843 1.589844 l 0.171875 -0.171875 l 0.007813 0.007812 l 4.300781 -4.300781 v -0.582031 z m 0 0"
+ fill="#2e3436"
+ id="path3525"
+ style="fill:#000000" />
+ <g
+ clip-path="url(#s)"
+ mask="url(#r)"
+ transform="matrix(1 0 0 1 -40 -620)"
+ id="g3529"
+ style="fill:#000000">
+ <path
+ d="m 181.503906 635.25 h 2.25 v 9 h -2.25 z m 0 0"
+ fill="#2e3436"
+ id="path3527"
+ style="fill:#000000" />
+ </g>
+ <path
+ d="m 5 14 c -1.105469 0 -2 0.894531 -2 2 h 10 c 0 -1.105469 -0.894531 -2 -2 -2 z m 0 0"
+ fill="#2e3436"
+ id="path3531"
+ style="fill:#000000" />
+</svg>
diff --git a/.config/ags/assets/processor-symbolic.svg b/.config/ags/assets/processor-symbolic.svg
new file mode 100644
index 0000000..832dbaf
--- /dev/null
+++ b/.config/ags/assets/processor-symbolic.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 5 5 h 6 v 6 h -6 z m 0 0"/>
+ <path d="m 13 5 h 3 v 1 h -3 z m 0 0"/>
+ <path d="m 13 7 h 3 v 1 h -3 z m 0 0"/>
+ <path d="m 13 9 h 3 v 1 h -3 z m 0 0"/>
+ <path d="m 0 6 h 3 v 1 h -3 z m 0 0"/>
+ <path d="m 0 8 h 3 v 1 h -3 z m 0 0"/>
+ <path d="m 0 10 h 3 v 1 h -3 z m 0 0"/>
+ <path d="m 5 0 h 1 v 3 h -1 z m 0 0"/>
+ <path d="m 7 0 h 1 v 3 h -1 z m 0 0"/>
+ <path d="m 9 0 h 1 v 3 h -1 z m 0 0"/>
+ <path d="m 10 13 h 1 v 3 h -1 z m 0 0"/>
+ <path d="m 8 13 h 1 v 3 h -1 z m 0 0"/>
+ <path d="m 6 13 h 1 v 3 h -1 z m 0 0"/>
+ <path d="m 5 2 c -1.644531 0 -3 1.355469 -3 3 v 6 c 0 1.644531 1.355469 3 3 3 h 6 c 1.644531 0 3 -1.355469 3 -3 v -6 c 0 -1.644531 -1.355469 -3 -3 -3 z m 0 2 h 6 c 0.570312 0 1 0.429688 1 1 v 6 c 0 0.570312 -0.429688 1 -1 1 h -6 c -0.570312 0 -1 -0.429688 -1 -1 v -6 c 0 -0.570312 0.429688 -1 1 -1 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/rotation.svg b/.config/ags/assets/rotation.svg
new file mode 100644
index 0000000..7ed7e34
--- /dev/null
+++ b/.config/ags/assets/rotation.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <path style="fill:currentColor" class="ColorScheme-Text" d="M 1 1 L 1 10 L 7 10 L 7 5 L 9 5 C 10.108 5 11 5.89199 11 7 L 11 9 L 8 9 L 8 11 L 6 11 L 6 15 L 15 15 L 15 9 L 12 9 L 12 7 C 12 5.338 10.662 4 9 4 L 7 4 L 7 1 L 1 1 z" transform="translate(4 4)"/>
+</svg>
diff --git a/.config/ags/assets/swapnext.svg b/.config/ags/assets/swapnext.svg
new file mode 100644
index 0000000..0690cb9
--- /dev/null
+++ b/.config/ags/assets/swapnext.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <path style="fill:currentColor" class="ColorScheme-Text" d="M 3 1 C 1.892 1 1 1.892 1 3 L 1 13 C 1 14.108 1.892 15 3 15 L 13 15 C 14.108 15 15 14.108 15 13 L 15 3 C 15 1.892 14.108 1 13 1 L 3 1 z M 3 3 L 13 3 L 13 13 L 3 13 L 3 3 z M 5 4 A 1 1 0 0 0 4 5 A 1 1 0 0 0 5 6 A 1 1 0 0 0 6 5 A 1 1 0 0 0 5 4 z M 11 4 A 1 1 0 0 0 10 5 A 1 1 0 0 0 11 6 A 1 1 0 0 0 12 5 A 1 1 0 0 0 11 4 z M 8 7 A 1 1 0 0 0 7 8 A 1 1 0 0 0 8 9 A 1 1 0 0 0 9 8 A 1 1 0 0 0 8 7 z M 5 10 A 1 1 0 0 0 4 11 A 1 1 0 0 0 5 12 A 1 1 0 0 0 6 11 A 1 1 0 0 0 5 10 z M 11 10 A 1 1 0 0 0 10 11 A 1 1 0 0 0 11 12 A 1 1 0 0 0 12 11 A 1 1 0 0 0 11 10 z" transform="translate(4 4)"/>
+</svg>
diff --git a/.config/ags/assets/tbox-close.svg b/.config/ags/assets/tbox-close.svg
new file mode 100644
index 0000000..f325d3c
--- /dev/null
+++ b/.config/ags/assets/tbox-close.svg
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="24"
+ height="24"
+ version="1.1"
+ id="svg1"
+ sodipodi:docname="tbox-close.svg"
+ xml:space="preserve"
+ inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
+ id="namedview1"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:zoom="26.708333"
+ inkscape:cx="11.981279"
+ inkscape:cy="11.981279"
+ inkscape:current-layer="svg1" /><defs
+ id="defs1"><style
+ id="current-color-scheme"
+ type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style></defs><g
+ id="g2"
+ transform="matrix(0.43167414,0,0,0.42254065,1.6211,1.3192164)"><path
+ style="opacity:0.2"
+ d="m 34.158694,7.001891 c -2.339941,-0.051114 -4.593769,0.9363666 -6.144072,2.6910582 -0.0012,0.0014 -0.0027,0.0025 -0.0039,0.0039 l -4.015616,4.4240828 -4.01174,-4.4162958 c -1.424465,-1.6183716 -3.450682,-2.592914 -5.603207,-2.6949526 -0.260519,-0.012616 -0.51772,-0.013001 -0.778224,0 a 5.977352,5.9824591 0 0 0 -0.01945,0 C 7.2433432,7.3455998 3.8467314,15.780704 8.1816032,20.422133 L 13.224489,25.975605 8.2594255,31.4434 c -1.5192901,1.580887 -2.4400962,4.078962 -2.2296097,6.266155 0.2104868,2.187194 1.2500876,3.924619 2.6187213,5.167923 1.3686339,1.243304 3.1976469,2.112373 5.3930859,2.110788 2.19544,-0.0016 4.590268,-1.161185 6.015666,-2.827365 l 3.937809,-4.330618 4.054544,4.463029 -0.147863,-0.167461 c 1.418645,1.683037 3.819828,2.866353 6.02734,2.874097 2.20751,0.0078 4.047562,-0.863731 5.420324,-2.110787 1.372761,-1.247055 2.417086,-2.995369 2.622612,-5.195185 0.205525,-2.199813 -0.739595,-4.705945 -2.276302,-6.281731 l -4.933937,-5.43664 5.050669,-5.561261 c 2.191468,-2.3538 2.626913,-5.893796 1.513644,-8.536614 C 40.212861,9.2349127 37.376326,7.0748944 34.162585,7.001891 a 5.977352,5.9824591 0 0 0 -0.0039,0 z"
+ id="path1-3" /><path
+ style="fill:#4f4f4f"
+ d="m 34.158694,6.0018906 c -2.339941,-0.051114 -4.593769,0.9363666 -6.144072,2.6910586 -0.0012,0.00139 -0.0027,0.0025 -0.0039,0.0039 L 23.995106,13.120932 19.983366,8.7046361 C 18.558901,7.0862642 16.532684,6.1117218 14.380159,6.0096832 c -0.260519,-0.012616 -0.51772,-0.013001 -0.778224,0 a 5.977352,5.9824591 0 0 0 -0.01945,0 C 7.2433432,6.3455994 3.8467314,14.780704 8.1816032,19.422133 L 13.224489,24.975605 8.2594255,30.4434 c -1.5192901,1.580887 -2.4400962,4.078962 -2.2296097,6.266155 0.2104868,2.187194 1.2500876,3.924619 2.6187213,5.167923 1.3686339,1.243304 3.1976469,2.112373 5.3930859,2.110788 2.19544,-0.0016 4.590268,-1.161185 6.015666,-2.827365 l 3.937809,-4.330618 4.054544,4.463029 -0.147863,-0.167461 c 1.418645,1.683037 3.819828,2.866353 6.02734,2.874097 2.20751,0.0078 4.047562,-0.863731 5.420324,-2.110787 1.372761,-1.247055 2.417086,-2.995369 2.622612,-5.195185 0.205525,-2.199813 -0.739595,-4.705945 -2.276302,-6.281731 l -4.933937,-5.43664 5.050669,-5.561261 c 2.191468,-2.3538 2.626913,-5.893796 1.513644,-8.536614 C 40.212861,8.2349123 37.376326,6.074894 34.162585,6.0018906 a 5.977352,5.9824591 0 0 0 -0.0039,0 z"
+ id="path2" /><path
+ style="opacity:0.2"
+ d="m 13.875,12.980111 a 2.0002,2.0002 0 0 0 -1.355469,3.363282 l 8.789063,9.667968 -8.769532,9.644532 A 2.0002,2.0002 0 1 0 15.5,38.343393 l 8.507812,-9.359374 8.51172,9.359374 a 2.0006762,2.0006762 0 1 0 2.960936,-2.6875 L 26.710938,26.011361 35.5,16.343393 a 2.0002,2.0002 0 0 0 -1.417968,-3.363282 2.0002,2.0002 0 0 0 -1.54297,0.675782 l -8.53125,9.382812 -8.527343,-9.382812 a 2.0002,2.0002 0 0 0 -1.40625,-0.675782 2.0002,2.0002 0 0 0 -0.199219,0 z"
+ id="path3" /><path
+ style="fill:#ffffff"
+ transform="scale(2)"
+ d="M 6.9375,5.9902344 A 1.0001,1.0001 0 0 0 6.2597656,7.671875 l 4.3945314,4.833984 -4.3847658,4.822266 A 1.0001,1.0001 0 1 0 7.75,18.671875 l 4.253906,-4.679687 4.25586,4.679687 a 1.0003381,1.0003381 0 1 0 1.480468,-1.34375 L 13.355469,12.505859 17.75,7.671875 A 1.0001,1.0001 0 0 0 17.041016,5.9902344 1.0001,1.0001 0 0 0 16.269531,6.328125 L 12.003906,11.019531 7.7402344,6.328125 a 1.0001,1.0001 0 0 0 -0.703125,-0.3378906 1.0001,1.0001 0 0 0 -0.099609,0 z"
+ id="path4" /><path
+ style="opacity:0.2;fill:#ffffff"
+ transform="scale(2)"
+ d="m 17.080078,3 c -1.16997,-0.025557 -2.297114,0.4683571 -3.072266,1.3457031 0,0 -0.002,0.00195 -0.002,0.00195 L 11.998047,6.5605469 9.9921875,4.3515625 C 9.279955,3.5423765 8.2657156,3.0549255 7.1894531,3.0039062 c -0.1302595,-0.00631 -0.2584199,-0.0065 -0.3886719,0 a 2.988676,2.9912295 0 0 0 -0.00977,0 C 4.516087,3.1244563 3.008504,5.3315905 3.1347612,7.4257812 3.2542394,5.4946775 4.6972247,3.6148577 6.7910112,3.5039062 a 2.988676,2.9912295 0 0 1 0.00977,0 c 0.130252,-0.0065 0.2584124,-0.00631 0.3886719,0 1.0762625,0.051019 2.0905019,0.5384705 2.8027344,1.3476563 l 2.0058595,2.2089844 2.007812,-2.2128907 c 0,0 0.002,-0.00195 0.002,-0.00195 C 14.782964,3.9683573 15.910108,3.474443 17.080078,3.5 a 2.988676,2.9912295 0 0 0 0.002,0 c 1.606871,0.036502 3.023444,1.1180442 3.580078,2.4394531 0.16598,0.3940229 0.255721,0.8292143 0.28125,1.2753907 0.03853,-0.6193247 -0.05357,-1.2348847 -0.28125,-1.7753907 C 20.105475,4.1180443 18.688902,3.0365017 17.082031,3 a 2.988676,2.9912295 0 0 0 -0.002,0 z M 6.3847656,12.738281 4.1289062,15.222656 c -0.7462708,0.776527 -1.199139,1.994686 -1.1152343,3.074219 0.063557,-0.939898 0.4824865,-1.915817 1.1152343,-2.574219 l 2.4824219,-2.734375 z m 11.2226564,0 -0.226563,0.25 2.466797,2.71875 c 0.646214,0.662647 1.079159,1.652595 1.140625,2.605469 0.08979,-1.091494 -0.380318,-2.325827 -1.140625,-3.105469 z"
+ id="path5" /></g></svg>
diff --git a/.config/ags/assets/terminal-symbolic.svg b/.config/ags/assets/terminal-symbolic.svg
new file mode 100644
index 0000000..9f82bcf
--- /dev/null
+++ b/.config/ags/assets/terminal-symbolic.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 2.199219 0 c -1.207031 0 -2.199219 1.007812 -2.199219 2.207031 v 10.585938 c 0 1.199219 0.992188 2.207031 2.199219 2.207031 h 11.601562 c 1.207031 0 2.199219 -1.007812 2.199219 -2.207031 v -10.585938 c 0 -1.199219 -0.992188 -2.207031 -2.199219 -2.207031 z m 0 2 h 11.601562 c 0.121094 0 0.199219 0.070312 0.199219 0.207031 v 10.585938 c 0 0.136719 -0.078125 0.207031 -0.199219 0.207031 h -11.601562 c -0.121094 0 -0.199219 -0.070312 -0.199219 -0.207031 v -10.585938 c 0 -0.136719 0.078125 -0.207031 0.199219 -0.207031 z m 0 0"/>
+ <path d="m 4.515625 5.898438 c -0.164063 -0.003907 -0.324219 0.0625 -0.441406 0.175781 c -0.230469 0.234375 -0.230469 0.617187 0 0.851562 l 1.578125 1.574219 l -1.578125 1.574219 c -0.230469 0.234375 -0.230469 0.617187 0 0.851562 c 0.234375 0.230469 0.617187 0.230469 0.851562 0 l 2 -2 c 0.230469 -0.234375 0.230469 -0.617187 0 -0.851562 l -2 -2 c -0.109375 -0.105469 -0.257812 -0.167969 -0.410156 -0.175781 z m 3.484375 4.101562 v 1 h 3 v -1 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/togglesplit.svg b/.config/ags/assets/togglesplit.svg
new file mode 100644
index 0000000..15d5011
--- /dev/null
+++ b/.config/ags/assets/togglesplit.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g transform="translate(4,4)">
+ <path style="fill:currentColor" class="ColorScheme-Text" d="M 1,1 V 10 H 5 V 5 H 10 V 1 Z M 6,6 H 15 V 15 H 6 Z"/>
+ </g>
+</svg>
diff --git a/.config/ags/assets/toolbars-symbolic.svg b/.config/ags/assets/toolbars-symbolic.svg
new file mode 100644
index 0000000..9f4c564
--- /dev/null
+++ b/.config/ags/assets/toolbars-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
+ <path d="m 2 0 c -1.214844 0 -2 0.828125 -2 2 v 12 c 0 1 1 2 2 2 h 11.984375 c 1 0 2 -1 2 -2 v -12 c 0 -1.238281 -0.828125 -2 -2 -2 z m 0 2 h 2 v 2 h -2 z m 3 0 h 2 v 2 h -2 z m 3 0 h 2 v 2 h -2 z m -6 4 h 11.984375 v 8 h -11.984375 z m 0 0"/>
+</svg>
diff --git a/.config/ags/assets/wp-next.svg b/.config/ags/assets/wp-next.svg
new file mode 100644
index 0000000..ac0245d
--- /dev/null
+++ b/.config/ags/assets/wp-next.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g transform="translate(4,4)">
+ <path style="fill:currentColor" class="ColorScheme-Text" d="M 3,2 V 14 L 14,8 Z"/>
+ </g>
+</svg>
diff --git a/.config/ags/assets/wp-prev.svg b/.config/ags/assets/wp-prev.svg
new file mode 100644
index 0000000..12ed8dd
--- /dev/null
+++ b/.config/ags/assets/wp-prev.svg
@@ -0,0 +1,10 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" version="1.1">
+ <defs>
+ <style id="current-color-scheme" type="text/css">
+ .ColorScheme-Text { color:#dfdfdf; } .ColorScheme-Highlight { color:#4285f4; } .ColorScheme-NeutralText { color:#ff9800; } .ColorScheme-PositiveText { color:#4caf50; } .ColorScheme-NegativeText { color:#f44336; }
+ </style>
+ </defs>
+ <g transform="translate(4,4)">
+ <path style="fill:currentColor" class="ColorScheme-Text" d="M 13,2 2,8 13,14 Z"/>
+ </g>
+</svg>
diff --git a/.config/ags/config.js b/.config/ags/config.js
new file mode 100644
index 0000000..2864dec
--- /dev/null
+++ b/.config/ags/config.js
@@ -0,0 +1,46 @@
+import GLib from "gi://GLib"
+
+const main = "/tmp/ags/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, 0],
+}
+
+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/.config/ags/default.nix b/.config/ags/default.nix
new file mode 100644
index 0000000..f0e0c41
--- /dev/null
+++ b/.config/ags/default.nix
@@ -0,0 +1,104 @@
+{
+ 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
+ inputs.hyprland.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/.config/ags/greeter.js b/.config/ags/greeter.js
new file mode 100644
index 0000000..5c8e369
--- /dev/null
+++ b/.config/ags/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/.config/ags/greeter/auth.ts b/.config/ags/greeter/auth.ts
new file mode 100644
index 0000000..23477eb
--- /dev/null
+++ b/.config/ags/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/.config/ags/greeter/greeter.scss b/.config/ags/greeter/greeter.scss
new file mode 100644
index 0000000..e3a5cd8
--- /dev/null
+++ b/.config/ags/greeter/greeter.scss
@@ -0,0 +1,64 @@
+@import "../style/mixins/floating-widget.scss";
+@import "../style/mixins/widget.scss";
+@import "../style/mixins/spacing.scss";
+@import "../style/mixins/unset.scss";
+@import "../style/mixins/a11y-button.scss";
+@import "../widget/bar/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/.config/ags/greeter/greeter.ts b/.config/ags/greeter/greeter.ts
new file mode 100644
index 0000000..eb1493f
--- /dev/null
+++ b/.config/ags/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/.config/ags/greeter/session.ts b/.config/ags/greeter/session.ts
new file mode 100644
index 0000000..092a5c2
--- /dev/null
+++ b/.config/ags/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/.config/ags/greeter/statusbar.ts b/.config/ags/greeter/statusbar.ts
new file mode 100644
index 0000000..8076011
--- /dev/null
+++ b/.config/ags/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/.config/ags/lib/battery.ts b/.config/ags/lib/battery.ts
new file mode 100644
index 0000000..3817260
--- /dev/null
+++ b/.config/ags/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/.config/ags/lib/client.js b/.config/ags/lib/client.js
new file mode 100644
index 0000000..9fb9164
--- /dev/null
+++ b/.config/ags/lib/client.js
@@ -0,0 +1,134 @@
+import Hyprland from 'resource:///com/github/Aylur/ags/service/hyprland.js';
+import { find_icon } from "./iconUtils.js";
+import { lookUpIcon, timeout } from 'resource:///com/github/Aylur/ags/utils.js';
+
+
+export let clientMapWorkSpace = {};
+
+export function substitute(str) {
+ const subs = [
+ { from: "code-url-handler", to: "visual-studio-code" },
+ { from: "Code", to: "visual-studio-code" },
+ { from: "GitHub Desktop", to: "github-desktop" },
+ { from: "wpsoffice", to: "wps-office2019-kprometheus" },
+ { from: "gnome-tweaks", to: "org.gnome.tweaks" },
+ { from: "Minecraft* 1.20.1", to: "minecraft" },
+ { from: "", to: "image-missing" },
+ ];
+
+ for (const { from, to } of subs) {
+ if (from === str) {
+ return to;
+ }
+ }
+
+ return str;
+}
+
+function titleToClient(title, className) {
+ const subs = [
+ { from: "musicfox", to: "musicfox" },
+ ];
+
+ for (const { from, to } of subs) {
+ if (title.indexOf(from) !== -1) {
+ return to;
+ }
+ }
+
+ return className
+}
+
+export const getClientByAdrees = function(address) {
+
+ const clients = Hyprland.clients
+
+ const client = clients.find(item => {
+ return item.address === address
+ })
+
+ return client
+}
+
+//Fullscreen client
+export const getFullScreenClientAddress = function(workspace_id) {
+
+ const clients = Hyprland.clients
+ const client = clients.find(item => {
+ return item.fullscreen && item.workspace.id === workspace_id
+ })
+ return client
+}
+
+export const ignoreAppsClass = [
+ 'image-missing',
+ 'fcitx',
+ 'rofi'
+]
+
+export const getClientIcon = (clientClass, title = "") => {
+
+ clientClass.toLowerCase()
+ clientClass = clientClass.replace(" ", "_");
+
+
+ if (title.length > 0) {
+ clientClass = titleToClient(title, clientClass)
+ }
+
+ const awesome_icon = find_icon(clientClass)
+ if (awesome_icon) {
+ return awesome_icon
+ }
+
+ if (lookUpIcon(clientClass)) {
+ return clientClass
+ }
+
+ if (find_icon('system')) {
+ return find_icon('system')
+ }
+
+ return ""
+}
+
+
+export const focus = (client) => {
+ //client
+ const { address } = client;
+ const liveClient = getClientByAdrees(address);
+
+ //special window
+ if (liveClient.workspace.id < 0) {
+ const oldWorkSpace = clientMapWorkSpace[address];
+ if (oldWorkSpace) {
+ Utils.exec(
+ `hyprctl dispatch movetoworkspace ${oldWorkSpace},address:${address}`,
+ );
+ Utils.exec(`hyprctl dispatch workspace ${oldWorkSpace}`);
+ }
+ }
+
+ //fullscreen
+ if (liveClient.fullscreen) {
+ Utils.exec("hyprctl dispatch focuswindow address:" + address);
+ return;
+ }
+
+ //workspace fullscreen client
+ const currentFullScreenAddress = getFullScreenClientAddress(
+ liveClient.workspace.id,
+ );
+ if (currentFullScreenAddress) {
+ const fullScreenAdress = currentFullScreenAddress.address;
+ Utils.exec("hyprctl dispatch focuswindow address:" + fullScreenAdress);
+ Utils.exec("hyprctl dispatch fullscreen 1");
+ }
+
+ Utils.exec("hyprctl dispatch focuswindow address:" + address);
+ // Utils.exec('hyprctl dispatch cyclenext')
+ Utils.exec("hyprctl dispatch alterzorder top,address:" + address);
+ if (currentFullScreenAddress) {
+ Utils.exec("hyprctl dispatch fullscreen 1");
+ }
+};
diff --git a/.config/ags/lib/cursorhover.js b/.config/ags/lib/cursorhover.js
new file mode 100644
index 0000000..d93d021
--- /dev/null
+++ b/.config/ags/lib/cursorhover.js
@@ -0,0 +1,86 @@
+const { Gdk, Gtk } = imports.gi;
+
+const CLICK_BRIGHTEN_AMOUNT = 0.13;
+
+export function setupCursorHover(button) {
+ const display = Gdk.Display.get_default();
+ button.connect('enter-notify-event', () => {
+ const cursor = Gdk.Cursor.new_from_name(display, 'pointer');
+ button.get_window().set_cursor(cursor);
+ });
+
+ button.connect('leave-notify-event', () => {
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ });
+
+}
+
+export function setupCursorHoverAim(button) {
+ button.connect('enter-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'crosshair');
+ button.get_window().set_cursor(cursor);
+ });
+
+ button.connect('leave-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ });
+}
+
+export function setupCursorHoverGrab(button) {
+ button.connect('enter-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'grab');
+ button.get_window().set_cursor(cursor);
+ });
+
+ button.connect('leave-notify-event', () => {
+ const display = Gdk.Display.get_default();
+ const cursor = Gdk.Cursor.new_from_name(display, 'default');
+ button.get_window().set_cursor(cursor);
+ });
+}
+
+// failed radial ripple experiment
+//
+// var clicked = false;
+// var dummy = false;
+// var cursorX = 0;
+// var cursorY = 0;
+// const styleContext = button.get_style_context();
+// var clickColor = styleContext.get_property('background-color', Gtk.StateFlags.HOVER);
+// clickColor.green += CLICK_BRIGHTEN_AMOUNT;
+// clickColor.blue += CLICK_BRIGHTEN_AMOUNT;
+// clickColor.red += CLICK_BRIGHTEN_AMOUNT;
+// clickColor = clickColor.to_string();
+// button.add_events(Gdk.EventMask.POINTER_MOTION_MASK);
+// button.connect('motion-notify-event', (widget, event) => {
+// [dummy, cursorX, cursorY] = event.get_coords(); // Get the mouse coordinates relative to the widget
+// if(!clicked) widget.css = `
+// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%);
+// `;
+// });
+
+// button.connect('button-press-event', (widget, event) => {
+// clicked = true;
+// [dummy, cursorX, cursorY] = event.get_coords(); // Get the mouse coordinates relative to the widget
+// cursorX = Math.round(cursorX); cursorY = Math.round(cursorY);
+// widget.css = `
+// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%);
+// `;
+// widget.toggleClassName('growingRadial', true);
+// widget.css = `
+// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, ${clickColor} 0%, ${clickColor} 0%, ${clickColor} 70%, ${clickColor} 70%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%);
+// `
+// });
+// button.connect('button-release-event', (widget, event) => {
+// widget.toggleClassName('growingRadial', false);
+// widget.toggleClassName('fadingRadial', false);
+// widget.css = `
+// background-image: radial-gradient(circle at ${cursorX}px ${cursorY}px, rgba(0,0,0,0), rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%, rgba(0,0,0,0) 70%);
+// `
+// clicked = false;
+// });
diff --git a/.config/ags/lib/gtk.ts b/.config/ags/lib/gtk.ts
new file mode 100644
index 0000000..8cd60a3
--- /dev/null
+++ b/.config/ags/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/.config/ags/lib/hyprland.ts b/.config/ags/lib/hyprland.ts
new file mode 100644
index 0000000..b121ed7
--- /dev/null
+++ b/.config/ags/lib/hyprland.ts
@@ -0,0 +1,80 @@
+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 activeBorder() {
+ const color = scheme.value === "dark"
+ ? darkActive.value
+ : lightActive.value
+
+ return color.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(${activeBorder()}ff)`,
+ `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/.config/ags/lib/iconUtils.js b/.config/ags/lib/iconUtils.js
new file mode 100644
index 0000000..db79553
--- /dev/null
+++ b/.config/ags/lib/iconUtils.js
@@ -0,0 +1,48 @@
+const { Gio, Gdk, Gtk } = imports.gi;
+
+function fileExists(filePath) {
+ let file = Gio.File.new_for_path(filePath);
+ return file.query_exists(null);
+}
+
+//计算多个数组的笛卡尔积
+function cartesianProduct(arrays) {
+ if (arrays.length === 0) {
+ return [[]];
+ }
+
+ const [head, ...tail] = arrays;
+ const tailCartesian = cartesianProduct(tail);
+ const result = [];
+
+ for (const item of head) {
+ for (const tailItem of tailCartesian) {
+ result.push([item, ...tailItem]);
+ }
+ }
+ return result;
+}
+
+export const find_icon = app_class => {
+ //主题路径 x 可能的尺寸大小 x apps x app的名称 x icon文件类型
+ const themPath = [
+ ['/usr/share/icons/WhiteSur/', '/usr/share/icons/WhiteSur-dark/'],
+ ['512x512/', '128x128/', '64x64/', '96x96/', '72x72/', '48x48/', '36x36/'],
+ ['apps/', ''],
+ [app_class + '.png', app_class + '.svg', app_class + '.xpm'],
+ ];
+
+ let real_path = '';
+ const all_icon_dir = cartesianProduct(themPath);
+
+ for (let index = 0; index < all_icon_dir.length; index++) {
+ const pathItem = all_icon_dir[index];
+ const icon_path = pathItem.join('');
+ if (fileExists(icon_path)) {
+ real_path = icon_path;
+ break;
+ }
+ }
+
+ return real_path;
+};
diff --git a/.config/ags/lib/icons.ts b/.config/ags/lib/icons.ts
new file mode 100644
index 0000000..317cb02
--- /dev/null
+++ b/.config/ags/lib/icons.ts
@@ -0,0 +1,185 @@
+export const substitutes = {
+ "transmission-gtk": "transmission",
+ "blueberry.py": "blueberry",
+ "Caprine": "facebook-messenger",
+ "phototonic": "terminal-symbolic",
+ "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-volume-medium-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-medium-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",
+ },
+ ui: {
+ arch: "archlinux-logo",
+ close: "window-close",
+ colorpicker: "color-select",
+ info: "info",
+ link: "external-link",
+ lock: "system-lock-screen",
+ menu: "open-menu",
+ refresh: "view-refresh",
+ search: "system-search",
+ settings: "emblem-system",
+ themes: "preferences-desktop-theme",
+ tick: "object-select",
+ time: "hourglass",
+ toolbars: "toolbars-symbolic",
+ warning: "dialog-warning",
+ avatar: "avatar-default",
+ tbox_osk: "osk",
+ tbox_appkill: "bomb-kill",
+ tbox_close: "tbox-close",
+ tbox_rotate: "rotation",
+ tbox_moveup: "arrows-up",
+ tbox_movedown: "arrows-down",
+ tbox_moveleft: "arrows-left",
+ tbox_moveright: "arrows-right",
+ tbox_workspacenext: "wp-next",
+ tbox_workspaceprev: "wp-prev",
+ tbox_fullscreen: "fullscreen",
+ tbox_swapnext: "swapnext",
+ tbox_float: "float",
+ tbox_pinned: "pinned",
+ tbox_split: "togglesplit",
+ arrow: {
+ right: "pan-end",
+ left: "pan-start",
+ down: "pan-down",
+ up: "pan-up",
+ },
+ },
+}
diff --git a/.config/ags/lib/init.ts b/.config/ags/lib/init.ts
new file mode 100644
index 0000000..aa03300
--- /dev/null
+++ b/.config/ags/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/.config/ags/lib/matugen.ts b/.config/ags/lib/matugen.ts
new file mode 100644
index 0000000..dfccccf
--- /dev/null
+++ b/.config/ags/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/.config/ags/lib/notifications.ts b/.config/ags/lib/notifications.ts
new file mode 100644
index 0000000..0000831
--- /dev/null
+++ b/.config/ags/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/.config/ags/lib/option.ts b/.config/ags/lib/option.ts
new file mode 100644
index 0000000..2d73978
--- /dev/null
+++ b/.config/ags/lib/option.ts
@@ -0,0 +1,115 @@
+import { Variable } from "resource:///com/github/Aylur/ags/variable.js"
+
+type OptProps = {
+ persistent?: boolean
+}
+
+export class Opt<T = unknown> extends Variable<T> {
+ static { Service.register(this) }
+
+ constructor(initial: T, { persistent = false }: OptProps = {}) {
+ super(initial)
+ this.initial = initial
+ this.persistent = persistent
+ }
+
+ initial: T
+ id = ""
+ persistent: boolean
+ toString() { 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 = <T>(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<T extends object>(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<Array<string>> {
+ 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/.config/ags/lib/session.ts b/.config/ags/lib/session.ts
new file mode 100644
index 0000000..0e3e0cf
--- /dev/null
+++ b/.config/ags/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/.config/ags/lib/tmux.ts b/.config/ags/lib/tmux.ts
new file mode 100644
index 0000000..1372eb2
--- /dev/null
+++ b/.config/ags/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"))
+ 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/.config/ags/lib/utils.ts b/.config/ags/lib/utils.ts
new file mode 100644
index 0000000..9b9d2a1
--- /dev/null
+++ b/.config/ags/lib/utils.ts
@@ -0,0 +1,111 @@
+/* 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<T> = import("types/service").Binding<any, any, T>
+
+/**
+ * @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).map(widget).flat(1)
+}
+
+/**
+ * @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 => {
+ return !Utils.exec(`which ${bin}`)
+ })
+
+ if (missing.length > 0) {
+ console.warn("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/.config/ags/lib/variables.ts b/.config/ags/lib/variables.ts
new file mode 100644
index 0000000..78d8793
--- /dev/null
+++ b/.config/ags/lib/variables.ts
@@ -0,0 +1,47 @@
+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 user = {
+ name: GLib.get_user_name()
+}
+
+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/.config/ags/main.ts b/.config/ags/main.ts
new file mode 100644
index 0000000..fa18846
--- /dev/null
+++ b/.config/ags/main.ts
@@ -0,0 +1,47 @@
+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"
+//import Dock from "widget/dock/Dock";
+import FloatingDock from "widget/dock/FloatingDock"
+import ToolBoxDock from "widget/dock/ToolBoxDock"
+
+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(Dock),
+ ...forMonitors(FloatingDock),
+ //...forMonitors(ToolBoxDock),
+ ...forMonitors(NotificationPopups),
+ ...forMonitors(ScreenCorners),
+ ...forMonitors(OSD),
+ Launcher(),
+ Overview(),
+ PowerMenu(),
+ SettingsDialog(),
+ Verification(),
+ ],
+})
diff --git a/.config/ags/options.ts b/.config/ags/options.ts
new file mode 100644
index 0000000..64c6580
--- /dev/null
+++ b/.config/ags/options.ts
@@ -0,0 +1,267 @@
+import { opt, mkOptions } from 'lib/option';
+import { distro } from 'lib/variables';
+import { icon } from 'lib/utils';
+import { icons } from "assets"
+import icons from 'lib/icons';
+//import Dock from "./widgets/dock/index.js";
+
+const options = mkOptions(OPTIONS, {
+ autotheme: opt(false),
+
+ wallpaper: {
+ resolution: opt<import('service/wallpaper').Resolution>(1920),
+ market: opt<import('service/wallpaper').Market>('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(100),
+ },
+
+ 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),
+ layout: {
+ start: opt<Array<import('widget/bar/Bar').BarWidget>>([
+ 'launcher',
+ 'workspaces',
+ //"taskbar",
+ 'expander',
+ 'messages',
+ ]),
+ center: opt<Array<import('widget/bar/Bar').BarWidget>>(['date']),
+ end: opt<Array<import('widget/bar/Bar').BarWidget>>([
+ "media",
+ 'expander',
+ //"colorpicker",
+ 'screenrecord',
+ 'battery',
+ 'systray',
+ 'system',
+ 'powermenu',
+ ]),
+ },
+ launcher: {
+ icon: {
+ colored: opt(true),
+ icon: opt(icon(distro.logo, icons.ui.search)),
+ },
+ label: {
+ colored: opt(false),
+ label: opt(''),
+ //label: opt(" Applications"),
+ },
+ action: opt(() => App.toggleWindow('launcher')),
+ },
+ date: {
+ format: opt('%a %d %b %Y %H:%M:%S'),
+ 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(6),
+ },
+ taskbar: {
+ iconSize: opt(0),
+ monochrome: opt(false),
+ exclusive: opt(false),
+ },
+ messages: {
+ action: opt(() => App.toggleWindow('datemenu')),
+ },
+ systray: {
+ ignore: opt([
+ 'KDE Connect Indicator',
+ //"spotify-client",
+ ]),
+ },
+ media: {
+ monochrome: opt(false),
+ preferred: opt('spotify'),
+ direction: opt<'left' | 'right'>('right'),
+ format: opt('{artists} - {title}'),
+ length: opt(40),
+ },
+ powermenu: {
+ monochrome: opt(false),
+ action: opt(() => App.toggleWindow('powermenu')),
+ },
+ },
+
+
+ dock: {
+ iconSize: opt(34),
+ pinnedApps: opt([
+ "nemo",
+ "firefox",
+ "qbittorrent",
+ "vlc",
+ "spotify",
+ "viewnior",
+ "lutris",
+ "steam",
+ "discord",
+ "code-oss",
+ "obsidian",
+ ]),
+ toolbox: {
+ icons: [
+ opt(icon(icons.ui.tbox_close)),
+ opt(icon(icons.ui.tbox_appkill)),
+ opt(icon(icons.ui.tbox_rotate)),
+ opt(icon(icons.ui.tbox_workspaceprev)),
+ opt(icon(icons.ui.tbox_workspacenext)),
+ opt(icon(icons.ui.tbox_moveleft)),
+ opt(icon(icons.ui.tbox_moveright)),
+ opt(icon(icons.ui.tbox_moveup)),
+ opt(icon(icons.ui.tbox_movedown)),
+ opt(icon(icons.ui.tbox_swapnext)),
+ opt(icon(icons.ui.tbox_split)),
+ opt(icon(icons.ui.tbox_float)),
+ opt(icon(icons.ui.tbox_pinned)),
+ opt(icon(icons.ui.tbox_fullscreen)),
+ opt(icon(icons.ui.tbox_osk)),
+ ]
+ },
+ },
+ 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', 'nemo', 'org.gnome.Calendar', 'obsidian', 'discord', 'spotify']]),
+ },
+ },
+
+ overview: {
+ scale: opt(9),
+ workspaces: opt(6),
+ monochromeIcon: opt(false),
+ },
+
+ 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(40),
+ },
+ 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<string>(JSON.parse(Utils.readFile(`${App.configDir}/.weather`) || '{}')?.key || ''),
+ cities: opt<Array<number>>(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<Array<'top' | 'bottom' | 'left' | 'right'>>(['top', 'right']),
+ blacklist: opt([""]),
+ //blacklist: opt(["Spotify"]),
+ width: opt(440),
+ },
+
+ hyprland: {
+ gaps: opt(0.5),
+ inactiveBorder: opt('333333ff'),
+ gapsWhenOnly: opt(false),
+ },
+});
+
+globalThis['options'] = options;
+export default options;
diff --git a/.config/ags/package.json b/.config/ags/package.json
new file mode 100644
index 0000000..e0586f7
--- /dev/null
+++ b/.config/ags/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "ags",
+ "author": "srdusr",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/srdusr/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/.config/ags/service/asusctl.ts b/.config/ags/service/asusctl.ts
new file mode 100644
index 0000000..16acbd7
--- /dev/null
+++ b/.config/ags/service/asusctl.ts
@@ -0,0 +1,52 @@
+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"],
+ })
+ }
+
+ available = !!Utils.exec("which asusctl")
+ #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/.config/ags/service/brightness.ts b/.config/ags/service/brightness.ts
new file mode 100644
index 0000000..a0b8eb5
--- /dev/null
+++ b/.config/ags/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")
+
+ 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/.config/ags/service/colorpicker.ts b/.config/ags/service/colorpicker.ts
new file mode 100644
index 0000000..5918f31
--- /dev/null
+++ b/.config/ags/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/.config/ags/service/nix.ts b/.config/ags/service/nix.ts
new file mode 100644
index 0000000..3bde9fc
--- /dev/null
+++ b/.config/ags/service/nix.ts
@@ -0,0 +1,109 @@
+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") }
+
+ 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/.config/ags/service/powermenu.ts b/.config/ags/service/powermenu.ts
new file mode 100644
index 0000000..fd16bc1
--- /dev/null
+++ b/.config/ags/service/powermenu.ts
@@ -0,0 +1,43 @@
+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 }
+ get cmd() { return this.#cmd }
+
+ 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")
+ }
+}
+
+const powermenu = new PowerMenu
+Object.assign(globalThis, { powermenu })
+export default powermenu
diff --git a/.config/ags/service/screenrecord.ts b/.config/ags/service/screenrecord.ts
new file mode 100644
index 0000000..58721d2
--- /dev/null
+++ b/.config/ags/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/.config/ags/service/wallpaper.ts b/.config/ags/service/wallpaper.ts
new file mode 100644
index 0000000..0e4bdda
--- /dev/null
+++ b/.config/ags/service/wallpaper.ts
@@ -0,0 +1,98 @@
+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",
+ "--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/.config/ags/service/weather.ts b/.config/ags/service/weather.ts
new file mode 100644
index 0000000..14f2df2
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/extra.scss b/.config/ags/style/extra.scss
new file mode 100644
index 0000000..e7f9d44
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/a11y-button.scss b/.config/ags/style/mixins/a11y-button.scss
new file mode 100644
index 0000000..00b24c6
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/button.scss b/.config/ags/style/mixins/button.scss
new file mode 100644
index 0000000..79ec275
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/floating-widget.scss b/.config/ags/style/mixins/floating-widget.scss
new file mode 100644
index 0000000..613668d
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/hidden.scss b/.config/ags/style/mixins/hidden.scss
new file mode 100644
index 0000000..ea6a42c
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/media.scss b/.config/ags/style/mixins/media.scss
new file mode 100644
index 0000000..3178029
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/scrollable.scss b/.config/ags/style/mixins/scrollable.scss
new file mode 100644
index 0000000..b66f246
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/slider.scss b/.config/ags/style/mixins/slider.scss
new file mode 100644
index 0000000..b90e566
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/spacing.scss b/.config/ags/style/mixins/spacing.scss
new file mode 100644
index 0000000..4096fba
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/switch.scss b/.config/ags/style/mixins/switch.scss
new file mode 100644
index 0000000..2abf360
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/mixins/unset.scss b/.config/ags/style/mixins/unset.scss
new file mode 100644
index 0000000..eb80af5
--- /dev/null
+++ b/.config/ags/style/mixins/unset.scss
@@ -0,0 +1,9 @@
+@mixin unset($rec: false) {
+ all: unset;
+
+ @if $rec {
+ * {
+ all: unset
+ }
+ }
+}
diff --git a/.config/ags/style/mixins/widget.scss b/.config/ags/style/mixins/widget.scss
new file mode 100644
index 0000000..053f1aa
--- /dev/null
+++ b/.config/ags/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/.config/ags/style/style.ts b/.config/ags/style/style.ts
new file mode 100644
index 0000000..a9b94fe
--- /dev/null
+++ b/.config/ags/style/style.ts
@@ -0,0 +1,103 @@
+/* eslint-disable max-len */
+import { type Opt } from "lib/option"
+import options from "options"
+import { bash, dependencies, sh } 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<any> | string, light: Opt<any> | string) => scheme.value === "dark"
+ ? `${dark}` : `${light}`
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const $ = (name: string, value: string | Opt<any>) => `$${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")),
+
+ $("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`
+ await Utils.writeFile(variables().join("\n"), vars)
+
+ const fd = await sh(`fd ".scss" ${App.configDir}`)
+ const files = fd.split(/\s+/).map(f => `@import '${f}';`)
+ const scss = [`@import '${vars}';`, ...files].join("\n")
+ const css = await bash`echo "${scss}" | sass --stdin`
+
+ App.applyCss(css, true)
+ } catch (error) {
+ logError(error)
+ }
+}
+
+Utils.monitorFile(App.configDir, resetCss)
+options.handler(deps, resetCss)
+await resetCss()
diff --git a/.config/ags/tsconfig.json b/.config/ags/tsconfig.json
new file mode 100644
index 0000000..1708aa3
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/PopupWindow.ts b/.config/ags/widget/PopupWindow.ts
new file mode 100644
index 0000000..b53b6fd
--- /dev/null
+++ b/.config/ags/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<WindowProps, "name"> & {
+ name: string
+ layout?: keyof ReturnType<typeof Layout>
+ 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<Gtk.Widget>({
+ 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/.config/ags/widget/RegularWindow.ts b/.config/ags/widget/RegularWindow.ts
new file mode 100644
index 0000000..1e4225d
--- /dev/null
+++ b/.config/ags/widget/RegularWindow.ts
@@ -0,0 +1,3 @@
+import Gtk from "gi://Gtk?version=3.0"
+
+export default Widget.subclass<typeof Gtk.Window, Gtk.Window.ConstructorProperties>(Gtk.Window)
diff --git a/.config/ags/widget/bar/Bar.ts b/.config/ags/widget/bar/Bar.ts
new file mode 100644
index 0000000..9343a36
--- /dev/null
+++ b/.config/ags/widget/bar/Bar.ts
@@ -0,0 +1,57 @@
+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 pos = options.bar.position.bind()
+
+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: pos.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]())),
+ }),
+ }),
+})
diff --git a/.config/ags/widget/bar/PanelButton.ts b/.config/ags/widget/bar/PanelButton.ts
new file mode 100644
index 0000000..332b46d
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/ScreenCorners.ts b/.config/ags/widget/bar/ScreenCorners.ts
new file mode 100644
index 0000000..1b35e50
--- /dev/null
+++ b/.config/ags/widget/bar/ScreenCorners.ts
@@ -0,0 +1,25 @@
+import options from "options"
+
+const { corners } = 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)
+ }),
+})
diff --git a/.config/ags/widget/bar/bar.scss b/.config/ags/widget/bar/bar.scss
new file mode 100644
index 0000000..5c6c2cd
--- /dev/null
+++ b/.config/ags/widget/bar/bar.scss
@@ -0,0 +1,242 @@
+@use 'sass:color';
+
+$bar-spacing: $spacing * 0.3;
+$button-radius: $radius;
+
+@mixin panel-button($flat: true, $reactive: true) {
+ @include accs-button($flat, $reactive);
+
+ >* {
+ border-radius: $button-radius;
+ margin: $bar-spacing;
+ background-color: $bg;
+ }
+
+ label,
+ image {
+ font-weight: bold;
+ }
+
+ >* {
+ padding: $padding * 0.8 $padding * 1.2;
+ }
+}
+
+.bar {
+ .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 * 0.6;
+ box-shadow: inset 0 0 0 $border-width $border-color;
+ margin: 0 $padding * 0.5;
+ transition: $transition * 0.5;
+ background-color: transparentize($fg, 0.8);
+
+ &.occupied {
+ background-color: transparentize($fg, 0.2);
+ min-width: 7pt;
+ min-height: 7pt;
+ }
+
+ &.active {
+ // background-color: $primary-bg;
+ background-image: $active-gradient;
+ min-width: 20pt;
+ min-height: 12pt;
+ }
+
+ &.inctive {
+ // background-color: $primary-bg;
+ background-image: $active-gradient;
+ min-width: 20pt;
+ min-height: 12pt;
+ }
+ }
+
+ &.inactive,
+ &:active {
+ label {
+ background-color: transparentize($primary-fg, 0.3);
+
+ &.occupied {
+ background-color: transparentize($primary-fg, 0.15);
+ }
+
+ &.active {
+ background-color: $primary-fg;
+ }
+
+ &.inactive {
+ background-color: $primary-fg;
+ }
+ }
+ }
+ }
+
+ .media label {
+ margin: 0 ($spacing * 0.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 * 0.7;
+ color: $fg;
+ text-shadow: $text-shadow;
+ }
+ }
+ }
+
+ .battery-bar {
+ >* {
+ padding: 0;
+ }
+
+ &.bar-hidden>box {
+ padding: 0 $spacing * 0.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 * 0.5;
+ }
+
+ trough {
+ @include widget;
+ min-height: 12pt;
+ min-width: 12pt;
+ }
+
+ .regular trough {
+ margin-right: $spacing * 0.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;
+ }
+ }
+}
diff --git a/.config/ags/widget/bar/buttons/BatteryBar.ts b/.config/ags/widget/bar/buttons/BatteryBar.ts
new file mode 100644
index 0000000..18de329
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/ColorPicker.ts b/.config/ags/widget/bar/buttons/ColorPicker.ts
new file mode 100644
index 0000000..5b1f3f6
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/Date.ts b/.config/ags/widget/bar/buttons/Date.ts
new file mode 100644
index 0000000..44c2540
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/Launcher.ts b/.config/ags/widget/bar/buttons/Launcher.ts
new file mode 100644
index 0000000..f3fee6b
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/Media.ts b/.config/ags/widget/bar/buttons/Media.ts
new file mode 100644
index 0000000..b3aab61
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/Messages.ts b/.config/ags/widget/bar/buttons/Messages.ts
new file mode 100644
index 0000000..a8971e9
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/PowerMenu.ts b/.config/ags/widget/bar/buttons/PowerMenu.ts
new file mode 100644
index 0000000..4432ade
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/ScreenRecord.ts b/.config/ags/widget/bar/buttons/ScreenRecord.ts
new file mode 100644
index 0000000..1d6eb36
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/SysTray.ts b/.config/ags/widget/bar/buttons/SysTray.ts
new file mode 100644
index 0000000..9f569d1
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/buttons/SystemIndicators.ts b/.config/ags/widget/bar/buttons/SystemIndicators.ts
new file mode 100644
index 0000000..cc98548
--- /dev/null
+++ b/.config/ags/widget/bar/buttons/SystemIndicators.ts
@@ -0,0 +1,107 @@
+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,
+ child: Widget.Icon({
+ icon: icons.bluetooth.enabled,
+ visible: bluetooth.bind('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(),
+ MicrophoneIndicator(),
+ AudioIndicator(),
+ NetworkIndicator(),
+ ]),
+ });
diff --git a/.config/ags/widget/bar/buttons/Taskbar.ts b/.config/ags/widget/bar/buttons/Taskbar.ts
new file mode 100644
index 0000000..b9c65fa
--- /dev/null
+++ b/.config/ags/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<T extends { attribute: { address: string } }>(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/.config/ags/widget/bar/buttons/Workspaces.ts b/.config/ags/widget/bar/buttons/Workspaces.ts
new file mode 100644
index 0000000..982ba13
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/bar/screencorner.scss b/.config/ags/widget/bar/screencorner.scss
new file mode 100644
index 0000000..93cd459
--- /dev/null
+++ b/.config/ags/widget/bar/screencorner.scss
@@ -0,0 +1,51 @@
+$_shadow-size: $padding;
+$_radius: $radius * $hyprland-gaps-multiplier;
+$_margin: 99px;
+
+window.screen-corner {
+ box.shadow {
+ margin-right: $_margin * -1;
+ margin-left: $_margin * -1;
+
+ @if $shadows {
+ box-shadow: inset 0 0 $_shadow-size 0 transparent;
+ }
+
+ @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 none $bg;
+ //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/.config/ags/widget/datemenu/DateColumn.ts b/.config/ags/widget/datemenu/DateColumn.ts
new file mode 100644
index 0000000..94e7051
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/datemenu/DateMenu.ts b/.config/ags/widget/datemenu/DateMenu.ts
new file mode 100644
index 0000000..f7fdf6d
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/datemenu/NotificationColumn.ts b/.config/ags/widget/datemenu/NotificationColumn.ts
new file mode 100644
index 0000000..07d6829
--- /dev/null
+++ b/.config/ags/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<number, ReturnType<typeof Animated>> = 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/.config/ags/widget/datemenu/datemenu.scss b/.config/ags/widget/datemenu/datemenu.scss
new file mode 100644
index 0000000..6fd9257
--- /dev/null
+++ b/.config/ags/widget/datemenu/datemenu.scss
@@ -0,0 +1,110 @@
+@import "../notifications/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/.config/ags/widget/desktop/Desktop.ts b/.config/ags/widget/desktop/Desktop.ts
new file mode 100644
index 0000000..f711967
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/dock/Dock.ts b/.config/ags/widget/dock/Dock.ts
new file mode 100644
index 0000000..c55f89f
--- /dev/null
+++ b/.config/ags/widget/dock/Dock.ts
@@ -0,0 +1,150 @@
+import { launchApp } from "lib/utils";
+import options from "options";
+import * as Gtk from "gi://Gtk?version=3.0";
+import { type ButtonProps } from "types/widgets/button";
+import { type BoxProps } from "types/widgets/box";
+
+const hyprland = await Service.import("hyprland");
+const applications = await Service.import("applications");
+
+const focus = (address: string) => hyprland.messageAsync(`dispatch focuswindow address:${address}`);
+
+const AppButton = ({ icon, pinned = false, term, ...rest }: ButtonProps & { term?: string }): Gtk.Button & ButtonProps => {
+ const { iconSize } = options.dock;
+
+ const buttonBox = Widget.Box({
+ class_name: 'box',
+ child: Widget.Icon({
+ icon,
+ size: iconSize,
+ }),
+ });
+
+ const button = Widget.Button({
+ ...rest,
+ class_name: '',
+ child: pinned ? buttonBox : Widget.Overlay({
+ child: buttonBox,
+ pass_through: false,
+ overlays: [],
+ }),
+ });
+
+ return Object.assign(button, {});
+};
+
+const createAppButton = ({ app, term, ...params }) => {
+ return AppButton({
+ icon: app.icon_name || '',
+ term,
+ ...params,
+ });
+};
+
+const filterValidClients = (clients: any[]) => {
+ return clients.filter(client => (
+ client.mapped &&
+ [client.class, client.title, client.initialClass].every(prop => typeof prop === 'string' && prop !== '')
+ ));
+};
+
+const Taskbar = (): Gtk.Box & BoxProps => {
+ const addedApps = new Set<string>();
+
+ const updateTaskbar = (clients: any[]) => {
+ const validClients = filterValidClients(clients);
+
+ const focusedAddress = hyprland?.active.client?.address;
+ const running = validClients.filter(client => client.mapped);
+ const focused = running.find(client => client.address === focusedAddress);
+
+ return validClients.map(client => {
+ if (![client.class, client.title, client.initialClass].every(prop => typeof prop === 'string' && prop !== '')) {
+ return null;
+ }
+
+ if (addedApps.has(client.title)) {
+ return null;
+ }
+
+ for (const appName of options.dock.pinnedApps.value) {
+ if (!appName || typeof appName !== 'string') {
+ continue;
+ }
+
+ if (client.class.includes(appName) || client.title.includes(appName)
+ || client.initialClass.includes(appName)) {
+ return null;
+ }
+ }
+
+ const matchingApp = applications?.list.find(app => (
+ app.match(client.title) || app.match(client.class) || app.match(client.initialClass)
+ ));
+
+ if (matchingApp) {
+ addedApps.add(client.title);
+ return createAppButton({
+ app: matchingApp,
+ term: matchingApp.title,
+ on_primary_click: () => {
+ const clickAddress = client.address || focusedAddress;
+ clickAddress && focus(clickAddress);
+ },
+ on_secondary_click: () => launchApp(matchingApp),
+ });
+ a
+ }
+ return null;
+ });
+ };
+
+ return Widget.Box({
+ vertical: false,
+ })
+ .bind('children', hyprland, 'clients', updateTaskbar);
+};
+
+const PinnedApps = (): Gtk.Box & BoxProps => {
+ const updatePinnedApps = (pinnedApps: string[]) => {
+ return pinnedApps
+ .map(term => ({ app: applications?.query(term)?.[0], term }))
+ .filter(({ app }) => app)
+ .map(({ app, term = true }) => createAppButton({
+ app,
+ term,
+ pinned: true,
+ on_primary_click: () => {
+ const matchingClients = hyprland?.clients.filter(client => (
+ typeof client.class === 'string' &&
+ typeof client.title === 'string' &&
+ typeof client.initialClass === 'string' &&
+ (client.class.includes(term) || client.title.includes(term) || client.initialClass.includes(term))
+ ));
+
+ if (matchingClients.length > 0) {
+ const { address } = matchingClients[0];
+ address && focus(address);
+ } else {
+ launchApp(app);
+ }
+ },
+ on_secondary_click: () => launchApp(app),
+ }));
+ };
+
+ return Widget.Box({
+ class_name: 'pins',
+ vertical: false,
+ homogeneous: true,
+ })
+ .bind('children', options.dock.pinnedApps, 'value', updatePinnedApps);
+};
+
+const Dock = (): Gtk.Box & BoxProps => Widget.Box({
+ class_name: 'dock',
+ vertical: false,
+ children: [PinnedApps(), Taskbar()],
+});
+
+export default Dock;
diff --git a/.config/ags/widget/dock/FloatingDock.ts b/.config/ags/widget/dock/FloatingDock.ts
new file mode 100644
index 0000000..98ff7cc
--- /dev/null
+++ b/.config/ags/widget/dock/FloatingDock.ts
@@ -0,0 +1,78 @@
+import options from "options"
+import Dock from "./Dock.ts"
+const hyprland = await Service.import("hyprland")
+const apps = await Service.import("applications")
+
+const { Gdk, Gtk } = imports.gi;
+import type Gtk from "gi://Gtk?version=3.0";
+import { type WindowProps } from "types/widgets/window";
+import { type RevealerProps } from "types/widgets/revealer";
+import { type EventBoxProps } from "types/widgets/eventbox";
+
+/** @param {number} monitor */
+const FloatingDock = (monitor: number): Gtk.Window & WindowProps => {
+ const update = () => {
+ const ws = hyprland.getWorkspace(hyprland.active.workspace.id);
+ if (hyprland.getMonitor(monitor)?.name === ws?.monitor) {
+ revealer.reveal_child = !ws || ws.windows === 0;
+ }
+ };
+
+ const revealer: Gtk.Revealer & RevealerProps = Widget.Revealer({
+ transition: 'slide_up',
+ transitionDuration: 90,
+ child: Dock(),
+ setup: self => self
+ .hook(hyprland, update, 'client-added')
+ .hook(hyprland, update, 'client-removed')
+ .hook(hyprland.active.workspace, update),
+ });
+
+ const window = Widget.Window({
+ monitor,
+ //halign: 'fill',
+ halign: 'end',
+ //layer: "overlay",
+ layer: "dock",
+ name: `dock${monitor}`,
+ click_through: false,
+ class_name: 'floating-dock',
+ // class_name: 'floating-dock-no-gap',
+ // class_name: "f-dock-wrap",
+
+
+ typeHint: Gdk.WindowTypeHint.DOCK,
+ exclusivity: 'false',
+
+ anchor: ['bottom'],
+ child: Widget.Box({
+ vertical: false,
+ halign: 'bottom',
+ hpack: 'start',
+ children: [
+ revealer,
+ Widget.Box({
+ class_name: 'padding',
+ css: 'padding: 9px; margin: 0;',
+ vertical: false,
+ halign: 'bottom',
+ hpack: 'start',
+ }),
+ ],
+ }),
+ });
+
+ window
+ .on('enter-notify-event', () => {
+ revealer.reveal_child = true;
+ })
+ .on('leave-notify-event', () => {
+ revealer.reveal_child = false;
+ })
+ .bind('visible', options.bar.position, 'value', v => v !== 'left');
+
+ return window;
+};
+
+export default FloatingDock;
+
diff --git a/.config/ags/widget/dock/ToolBox.ts b/.config/ags/widget/dock/ToolBox.ts
new file mode 100644
index 0000000..51fda72
--- /dev/null
+++ b/.config/ags/widget/dock/ToolBox.ts
@@ -0,0 +1,122 @@
+import options from "options";
+import { sh } from "lib/utils";
+import * as Gtk from "gi://Gtk?version=3.0";
+import { type ButtonProps } from "types/widgets/button";
+import { type BoxProps } from "types/widgets/box";
+
+const hyprland = await Service.import("hyprland");
+const { icons } = options.dock.toolbox;
+const buttonToggles = {};
+
+const dispatch = (action: string, arg: string) => {
+ //console.log(`Performing action: ${action} with argument: ${arg}`);
+ sh(`hyprctl dispatch ${action} ${arg}`);
+};
+
+const keyword = (action: string, arg: string) => {
+ //console.log(`Performing action: ${action} with argument: ${arg}`);
+ sh(`hyprctl keyword ${action} ${arg}`);
+};
+
+const ToggleSwitch = (buttonIndex, actionOn, argOn, actionOff, argOff, actionExec) => {
+ buttonToggles[buttonIndex] = !buttonToggles[buttonIndex];
+ const { action, arg } = buttonToggles[buttonIndex] ? { action: actionOn, arg: argOn } : { action: actionOff, arg: argOff };
+ actionExec(action, arg);
+};
+
+const ToggleOnMulti = (buttonIndex, actionOn, argOn, actionOff, argOff, actionExec) => {
+ buttonToggles[buttonIndex] = !buttonToggles[buttonIndex];
+ if (buttonToggles[buttonIndex]) {
+ actionOn.forEach(({ action, arg }) => {
+ actionExec(action, arg);
+ });
+ } else {
+ actionExec(actionOff, argOff);
+ }
+};
+
+const execAction = (trigger, actionIndex, actionOn, argOn, actionOff, argOff, action, arg, actionExec) => {
+ switch (trigger) {
+ case 'toggleOn-multi':
+ ToggleOnMulti(actionIndex, actionOn, argOn, actionOff, argOff, actionExec);
+ break;
+ case 'toggle-switch':
+ ToggleSwitch(actionIndex, actionOn, argOn, actionOff, argOff, actionExec);
+ break;
+ case 'oneshot':
+ actionExec(action, arg);
+ break;
+ default:
+ break;
+ }
+};
+
+const buttonConfigs = [
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 0, action: 'killactive', arg: '' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 1, action: 'exec hyprctl', arg: 'kill' },
+ {
+ actionExec: keyword,
+ trigger: 'toggle-switch',
+ actionIndex: 2,
+ actionOn: 'monitor', argOn: 'eDP-1,2736x1824,0x0,0,transform,1',
+ actionOff: 'monitor', argOff: 'eDP-1,2736x1824,0x0,0,transform,0'
+ },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 3, action: 'workspace', arg: 'r-1' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 4, action: 'workspace', arg: 'r+1' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 5, action: 'movewindow', arg: 'l' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 6, action: 'movewindow', arg: 'r' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 7, action: 'movewindow', arg: 'u' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 8, action: 'movewindow', arg: 'd' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 9, action: 'swapnext', arg: 'next' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 10, action: 'togglesplit', arg: '' },
+ {
+ actionExec: dispatch,
+ trigger: 'toggleOn-multi',
+ actionIndex: 11,
+ actionOn: [
+ { action: 'setfloating', arg: 'active' },
+ { action: 'resizeactive', arg: 'exact 90% 90%' },
+ { action: 'centerwindow', arg: '' },
+ ],
+ actionOff: 'settiled', argOff: 'active'
+ },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 12, action: 'pin', arg: '' },
+ { actionExec: dispatch, trigger: 'oneshot', actionIndex: 13, action: 'fullscreen', arg: '0' },
+ {
+ actionExec: dispatch,
+ trigger: 'toggle-switch',
+ actionIndex: 14,
+ actionOn: 'exec', argOn: 'wvctl 1',
+ actionOff: 'exec', argOff: 'wvctl 0'
+ },
+];
+
+const ToolBox = () => {
+ const ToolBoxButtons = () => {
+ const buttons = buttonConfigs.map(({ actionIndex, actionOn, argOn, actionOff, argOff, actionExec, trigger, action, arg }) => {
+ const execActionWrapper = () => execAction(trigger, actionIndex, actionOn, argOn, actionOff, argOff, action, arg, actionExec);
+
+ return Widget.Button({
+ child: Widget.Icon({
+ icon: icons[actionIndex].bind(),
+ }),
+ on_clicked: execActionWrapper,
+ });
+ });
+
+ return Widget.Box({
+ vertical: true,
+ homogeneous: true,
+ children: buttons,
+ });
+ };
+
+ return Widget.Box({
+ class_name: "toolbox",
+ vertical: true,
+ homogeneous: true,
+ children: [ToolBoxButtons()],
+ });
+};
+
+export default ToolBox;
diff --git a/.config/ags/widget/dock/ToolBoxDock.ts b/.config/ags/widget/dock/ToolBoxDock.ts
new file mode 100644
index 0000000..21beaeb
--- /dev/null
+++ b/.config/ags/widget/dock/ToolBoxDock.ts
@@ -0,0 +1,57 @@
+import options from "options";
+import ToolBox from "./ToolBox.ts";
+const hyprland = await Service.import("hyprland");
+const apps = await Service.import("applications");
+
+import type Gtk from "gi://Gtk?version=3.0";
+import { type WindowProps } from "types/widgets/window";
+import { type RevealerProps } from "types/widgets/revealer";
+import { type EventBoxProps } from "types/widgets/eventbox";
+
+/** @param {number} monitor */
+const ToolBoxDock = (monitor: number): Gtk.Window & WindowProps => {
+
+ const revealer: Gtk.Revealer & RevealerProps = Widget.Revealer({
+ transition: 'slide_left',
+ transitionDuration: 50,
+ child: ToolBox(),
+ });
+
+ const window = Widget.Window({
+ monitor,
+ halign: 'fill',
+ layer: "overlay",
+ name: `toolbox${monitor}`,
+ click_through: false,
+ class_name: 'floating-toolbox',
+ anchor: ['right'],
+ child: Widget.Box({
+ vertical: true,
+ halign: 'top',
+ hpack: 'fill',
+ children: [
+ revealer,
+ Widget.Box({
+ class_name: 'padding',
+ css: 'padding: 14px;',
+ vertical: true,
+ halign: 'top',
+ hpack: 'fill',
+ }),
+ ],
+ }),
+ });
+
+ window
+ .on('enter-notify-event', () => {
+ revealer.reveal_child = true;
+ })
+ .on('leave-notify-event', () => {
+ revealer.reveal_child = false;
+ })
+ .bind('visible', options.bar.position, 'value', v => v !== 'left');
+
+ return window;
+};
+
+export default ToolBoxDock;
diff --git a/.config/ags/widget/dock/dock.scss b/.config/ags/widget/dock/dock.scss
new file mode 100644
index 0000000..9dc6256
--- /dev/null
+++ b/.config/ags/widget/dock/dock.scss
@@ -0,0 +1,73 @@
+@use 'sass:color';
+
+.floating-dock {
+ padding-left: 0.2rem;
+ padding-right: 0.2rem;
+ padding-top: 0.3rem;
+ padding-bottom: 0.3rem;
+ border-radius: 1rem;
+}
+
+.dock {
+ // @include floating-widget;
+ border-radius: $radius;
+ background-color: transparentize($bg, 0.07);
+ min-width: 0;
+ padding: 6;
+
+ // Common styles for both PinnedApps and Taskbar buttons
+ button {
+ @include button($flat: true);
+ border-radius: 4;
+ padding: 2;
+
+ .box {
+ margin: 0em;
+ min-width: 1em;
+ min-height: 0em;
+ padding: 0;
+ }
+
+ image {
+ margin: 0px;
+ }
+
+ .indicator {
+ background-color: transparentize($primary-bg, 0.3);
+ border-radius: $radius;
+ min-height: 1pt;
+ min-width: 16pt;
+ margin: 1pt;
+ }
+ }
+}
+
+.toolbox {
+ // @include floating-widget;
+ border-radius: $radius;
+ background-color: transparentize($bg, 0.07);
+ min-width: 0;
+ padding: 6;
+
+ // Common styles for both PinnedApps and Taskbar buttons
+ button {
+ @include button($flat: true);
+ border-radius: 0;
+ padding: 0;
+
+ image {
+ margin: 0px;
+ font-size: 32px;
+ }
+
+ &:hover,
+ &:active,
+ &:focus {
+ // Override hover, active, and focus styles for buttons in .toolbox
+ background-color: transparent;
+ border: none;
+ box-shadow: none;
+ outline: none;
+ }
+ }
+}
diff --git a/.config/ags/widget/launcher/AppLauncher.ts b/.config/ags/widget/launcher/AppLauncher.ts
new file mode 100644
index 0000000..4d7ca73
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/launcher/Launcher.ts b/.config/ags/widget/launcher/Launcher.ts
new file mode 100644
index 0000000..3b73dc5
--- /dev/null
+++ b/.config/ags/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<string>) {
+ 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 = ""
+ 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/.config/ags/widget/launcher/NixRun.ts b/.config/ags/widget/launcher/NixRun.ts
new file mode 100644
index 0000000..cec9e09
--- /dev/null
+++ b/.config/ags/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<typeof Widget.Label>,
+ subpkg as ReturnType<typeof Widget.Label>,
+ ]),
+ ),
+ ),
+ )
+}
+
+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<ReturnType<typeof Item>>({
+ 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/.config/ags/widget/launcher/ShRun.ts b/.config/ags/widget/launcher/ShRun.ts
new file mode 100644
index 0000000..c4215ef
--- /dev/null
+++ b/.config/ags/widget/launcher/ShRun.ts
@@ -0,0 +1,89 @@
+import icons from "lib/icons"
+import options from "options"
+import { bash, dependencies } from "lib/utils"
+
+const iconVisible = Variable(false)
+
+const MAX = options.launcher.sh.max
+const BINS = `${Utils.CACHE_DIR}/binaries`
+bash("{ IFS=:; ls -H $PATH; } | sort ")
+ .then(bins => Utils.writeFile(bins, 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)
+ })
+}
+
+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<ReturnType<typeof Item>>({
+ 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 query(term)
+ list.children = found.map(Item)
+ revealer.reveal_child = true
+ }
+ }
+
+ return Object.assign(revealer, { filter, run })
+}
diff --git a/.config/ags/widget/launcher/launcher.scss b/.config/ags/widget/launcher/launcher.scss
new file mode 100644
index 0000000..926abc3
--- /dev/null
+++ b/.config/ags/widget/launcher/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/.config/ags/widget/notifications/Notification.ts b/.config/ags/widget/notifications/Notification.ts
new file mode 100644
index 0000000..c1c8dd8
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/notifications/NotificationPopups.ts b/.config/ags/widget/notifications/NotificationPopups.ts
new file mode 100644
index 0000000..a4a2b54
--- /dev/null
+++ b/.config/ags/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<number, ReturnType<typeof Animated>> = 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/.config/ags/widget/notifications/notifications.scss b/.config/ags/widget/notifications/notifications.scss
new file mode 100644
index 0000000..369932f
--- /dev/null
+++ b/.config/ags/widget/notifications/notifications.scss
@@ -0,0 +1,79 @@
+@mixin notification() {
+ &.critical {
+ box-shadow: inset 0 0 0.5em 0 $error-bg;
+ }
+
+ &:hover button.close-button {
+ @include button-hover;
+ background-color: transparentize($error-bg, 0.5);
+ }
+
+ .content {
+ .title {
+ margin-right: $spacing;
+ color: $fg;
+ font-size: 1.1em;
+ }
+
+ .time {
+ color: transparentize($fg, 0.2);
+ }
+
+ .description {
+ font-size: 0.9em;
+ color: transparentize($fg, 0.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, 0.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: 200px;
+ }
+ }
+}
diff --git a/.config/ags/widget/osd/OSD.ts b/.config/ags/widget/osd/OSD.ts
new file mode 100644
index 0000000..8239a08
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/osd/Progress.ts b/.config/ags/widget/osd/Progress.ts
new file mode 100644
index 0000000..bcf27da
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/osd/osd.scss b/.config/ags/widget/osd/osd.scss
new file mode 100644
index 0000000..111a486
--- /dev/null
+++ b/.config/ags/widget/osd/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/.config/ags/widget/overview/Overview.ts b/.config/ags/widget/overview/Overview.ts
new file mode 100644
index 0000000..8911920
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/overview/Window.ts b/.config/ags/widget/overview/Window.ts
new file mode 100644
index 0000000..02f71eb
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/overview/Workspace.ts b/.config/ags/widget/overview/Workspace.ts
new file mode 100644
index 0000000..1b8d60b
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/overview/overview.scss b/.config/ags/widget/overview/overview.scss
new file mode 100644
index 0000000..c0c4b13
--- /dev/null
+++ b/.config/ags/widget/overview/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/.config/ags/widget/powermenu/PowerMenu.ts b/.config/ags/widget/powermenu/PowerMenu.ts
new file mode 100644
index 0000000..fe0a0e9
--- /dev/null
+++ b/.config/ags/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<Gtk.Widget>({
+ 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/.config/ags/widget/powermenu/Verification.ts b/.config/ags/widget/powermenu/Verification.ts
new file mode 100644
index 0000000..e85c81a
--- /dev/null
+++ b/.config/ags/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: () => Utils.exec(powermenu.cmd),
+ }),
+ ],
+ }),
+ ],
+ }),
+})
diff --git a/.config/ags/widget/powermenu/powermenu.scss b/.config/ags/widget/powermenu/powermenu.scss
new file mode 100644
index 0000000..d5ce0de
--- /dev/null
+++ b/.config/ags/widget/powermenu/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/.config/ags/widget/quicksettings/QuickSettings.ts b/.config/ags/widget/quicksettings/QuickSettings.ts
new file mode 100644
index 0000000..2c0d6ac
--- /dev/null
+++ b/.config/ags/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, Microhone, 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],
+ ),
+ Microhone(),
+ 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/.config/ags/widget/quicksettings/ToggleButton.ts b/.config/ags/widget/quicksettings/ToggleButton.ts
new file mode 100644
index 0000000..62a2e67
--- /dev/null
+++ b/.config/ags/widget/quicksettings/ToggleButton.ts
@@ -0,0 +1,154 @@
+import { type Props as IconProps } from "types/widgets/icon"
+import { type Props as 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/.config/ags/widget/quicksettings/quicksettings.scss b/.config/ags/widget/quicksettings/quicksettings.scss
new file mode 100644
index 0000000..bd18ff1
--- /dev/null
+++ b/.config/ags/widget/quicksettings/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/.config/ags/widget/quicksettings/widgets/Bluetooth.ts b/.config/ags/widget/quicksettings/widgets/Bluetooth.ts
new file mode 100644
index 0000000..649e654
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/quicksettings/widgets/Brightness.ts b/.config/ags/widget/quicksettings/widgets/Brightness.ts
new file mode 100644
index 0000000..a3ce565
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/quicksettings/widgets/DND.ts b/.config/ags/widget/quicksettings/widgets/DND.ts
new file mode 100644
index 0000000..7fc1fd0
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/quicksettings/widgets/DarkMode.ts b/.config/ags/widget/quicksettings/widgets/DarkMode.ts
new file mode 100644
index 0000000..9ec94df
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/quicksettings/widgets/Header.ts b/.config/ags/widget/quicksettings/widgets/Header.ts
new file mode 100644
index 0000000..44c26f2
--- /dev/null
+++ b/.config/ags/widget/quicksettings/widgets/Header.ts
@@ -0,0 +1,69 @@
+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.Label({ label: `${user.name}\n` }),
+ // //Widget.Label({ label: uptime.bind().value }),
+ // Widget.Label({ label: `${user.name}\n ${uptime.bind().value}` }),
+ // //Widget.Icon({ icon: icons.ui.time }),
+ ]),
+
+ ],
+ }),
+ 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/.config/ags/widget/quicksettings/widgets/Media.ts b/.config/ags/widget/quicksettings/widgets/Media.ts
new file mode 100644
index 0000000..52254ea
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/quicksettings/widgets/MicMute.ts b/.config/ags/widget/quicksettings/widgets/MicMute.ts
new file mode 100644
index 0000000..b6e9454
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/quicksettings/widgets/Network.ts b/.config/ags/widget/quicksettings/widgets/Network.ts
new file mode 100644
index 0000000..eb14ab4
--- /dev/null
+++ b/.config/ags/widget/quicksettings/widgets/Network.ts
@@ -0,0 +1,61 @@
+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.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/.config/ags/widget/quicksettings/widgets/PowerProfile.ts b/.config/ags/widget/quicksettings/widgets/PowerProfile.ts
new file mode 100644
index 0000000..f566aaf
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/quicksettings/widgets/Volume.ts b/.config/ags/widget/quicksettings/widgets/Volume.ts
new file mode 100644
index 0000000..6d18d1a
--- /dev/null
+++ b/.config/ags/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 Microhone = () => 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/.config/ags/widget/settings/Group.ts b/.config/ags/widget/settings/Group.ts
new file mode 100644
index 0000000..e9356e0
--- /dev/null
+++ b/.config/ags/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<typeof Row<any>>[]) => 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/.config/ags/widget/settings/Page.ts b/.config/ags/widget/settings/Page.ts
new file mode 100644
index 0000000..220e560
--- /dev/null
+++ b/.config/ags/widget/settings/Page.ts
@@ -0,0 +1,19 @@
+import Group from "./Group"
+
+export default <T>(
+ name: string,
+ icon: string,
+ ...groups: ReturnType<typeof Group<T>>[]
+) => 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/.config/ags/widget/settings/Row.ts b/.config/ags/widget/settings/Row.ts
new file mode 100644
index 0000000..1e17096
--- /dev/null
+++ b/.config/ags/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<T> = {
+ opt: Opt<T>
+ title: string
+ note?: string
+ type?:
+ | "number"
+ | "color"
+ | "float"
+ | "object"
+ | "string"
+ | "enum"
+ | "boolean"
+ | "img"
+ | "font"
+ enums?: string[]
+ max?: number
+ min?: number
+}
+
+export default <T>(props: RowProps<T>) => 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/.config/ags/widget/settings/Setter.ts b/.config/ags/widget/settings/Setter.ts
new file mode 100644
index 0000000..7e455c9
--- /dev/null
+++ b/.config/ags/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<string>, 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<T>({
+ opt,
+ type = typeof opt.value as RowProps<T>["type"],
+ enums,
+ max = 1000,
+ min = 0,
+}: RowProps<T>) {
+ 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<string>, 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/.config/ags/widget/settings/SettingsDialog.ts b/.config/ags/widget/settings/SettingsDialog.ts
new file mode 100644
index 0000000..be0c35e
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/settings/Wallpaper.ts b/.config/ags/widget/settings/Wallpaper.ts
new file mode 100644
index 0000000..998f3b7
--- /dev/null
+++ b/.config/ags/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/.config/ags/widget/settings/layout.ts b/.config/ags/widget/settings/layout.ts
new file mode 100644
index 0000000..2b45810
--- /dev/null
+++ b/.config/ags/widget/settings/layout.ts
@@ -0,0 +1,147 @@
+/* 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<typeof Row>,
+ 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.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" }),
+ ),
+ 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/.config/ags/widget/settings/settingsdialog.scss b/.config/ags/widget/settings/settingsdialog.scss
new file mode 100644
index 0000000..b8c9820
--- /dev/null
+++ b/.config/ags/widget/settings/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;
+ }
+ }
+ }
+}