├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app.ts ├── assets ├── defsong.jpg └── icons │ └── hicolor │ └── scalable │ ├── apps │ ├── dark-souls-remastered.png │ ├── umineko.png │ └── windowkill.png │ └── status │ ├── ds-battery-1-symbolic.svg │ ├── ds-battery-2-symbolic.svg │ ├── ds-battery-3-symbolic.svg │ ├── ds-battery-4-symbolic.svg │ ├── ds-battery-charging-symbolic.svg │ ├── ds-bell-off-symbolic.svg │ ├── ds-bell-symbolic.svg │ ├── ds-bluetooth-symbolic.svg │ ├── ds-calendar-symbolic.svg │ ├── ds-check-symbolic.svg │ ├── ds-chevron-down-symbolic.svg │ ├── ds-chevron-left-symbolic.svg │ ├── ds-chevron-right-symbolic.svg │ ├── ds-chevron-up-symbolic.svg │ ├── ds-clock-symbolic.svg │ ├── ds-cloud-drizzle-symbolic.svg │ ├── ds-cloud-fog-symbolic.svg │ ├── ds-cloud-lightning-symbolic.svg │ ├── ds-cloud-moon-rain-symbolic.svg │ ├── ds-cloud-moon-symbolic.svg │ ├── ds-cloud-rain-symbolic.svg │ ├── ds-cloud-snow-symbolic.svg │ ├── ds-cloud-sun-rain-symbolic.svg │ ├── ds-cloud-sun-symbolic.svg │ ├── ds-cloud-symbolic.svg │ ├── ds-cloudy-symbolic.svg │ ├── ds-droplet-symbolic.svg │ ├── ds-ethernet-port-symbolic.svg │ ├── ds-log-out-symbolic.svg │ ├── ds-map-pin-symbolic.svg │ ├── ds-mic-off-symbolic.svg │ ├── ds-mic-symbolic.svg │ ├── ds-moon-symbolic.svg │ ├── ds-music-symbolic.svg │ ├── ds-pause-symbolic.svg │ ├── ds-play-symbolic.svg │ ├── ds-power-symbolic.svg │ ├── ds-refresh-cw-symbolic.svg │ ├── ds-search-symbolic.svg │ ├── ds-skip-back-symbolic.svg │ ├── ds-skip-forward-symbolic.svg │ ├── ds-speedometer-1-symbolic.svg │ ├── ds-speedometer-2-symbolic.svg │ ├── ds-speedometer-3-symbolic.svg │ ├── ds-sun-symbolic.svg │ ├── ds-trash-2-symbolic.svg │ ├── ds-video-symbolic.svg │ ├── ds-volume-1-symbolic.svg │ ├── ds-volume-2-symbolic.svg │ ├── ds-volume-symbolic.svg │ ├── ds-volume-x-symbolic.svg │ ├── ds-wifi-1-symbolic.svg │ ├── ds-wifi-2-symbolic.svg │ ├── ds-wifi-3-symbolic.svg │ ├── ds-wifi-4-symbolic.svg │ ├── ds-wifi-5-symbolic.svg │ ├── ds-wifi-off-symbolic.svg │ └── ds-x-symbolic.svg ├── env.d.ts ├── meson.build ├── options.ts ├── package.json ├── request.ts ├── run-dev.sh ├── run.sh ├── src ├── lib │ ├── icons.ts │ ├── option.ts │ ├── timer.ts │ └── utils.ts ├── services │ ├── brightness.ts │ ├── cliphist.ts │ ├── powermenu.ts │ ├── screenrecord.ts │ ├── styles.ts │ └── weather.ts ├── styles │ ├── _extra.scss │ ├── bar.scss │ ├── calendar.scss │ ├── control.scss │ ├── launcher.scss │ ├── notifications.scss │ ├── osd.scss │ ├── powermenu.scss │ └── weather.scss └── widgets │ ├── bar │ ├── bar.tsx │ ├── items │ │ ├── clock.tsx │ │ ├── keyboard.tsx │ │ ├── keyboard │ │ │ ├── hypr.tsx │ │ │ └── niri.tsx │ │ ├── launcher.tsx │ │ ├── notifications.tsx │ │ ├── recordindicator.tsx │ │ ├── sysbox.tsx │ │ ├── tray.tsx │ │ ├── weather.tsx │ │ ├── workspaces.tsx │ │ └── workspaces │ │ │ ├── hypr.tsx │ │ │ └── niri.tsx │ └── shadow.tsx │ ├── calendar │ ├── calendar.tsx │ └── layout.ts │ ├── common │ ├── baritem.tsx │ ├── baritempopup.tsx │ ├── popupwindow.tsx │ └── qsbutton.tsx │ ├── control │ ├── control.tsx │ ├── items │ │ ├── media.tsx │ │ ├── qsbuttons.tsx │ │ └── sliders.tsx │ └── pages │ │ ├── bluetooth.tsx │ │ ├── main.tsx │ │ ├── network.tsx │ │ └── powermodes.tsx │ ├── launcher │ ├── items │ │ ├── app_button.tsx │ │ ├── clip_color.tsx │ │ ├── clip_image.tsx │ │ └── clip_text.tsx │ ├── launcher.tsx │ └── pages │ │ ├── applauncher.tsx │ │ └── clipboard.tsx │ ├── notifications │ ├── items │ │ └── notification.tsx │ ├── notificationpopup.tsx │ └── notificationslist.tsx │ ├── osd │ └── osd.tsx │ ├── powermenu │ ├── powermenu.tsx │ └── verification.tsx │ └── weather │ ├── items │ ├── current.tsx │ ├── days.tsx │ └── hours.tsx │ └── weather.tsx ├── tsconfig.json └── windows.tsx /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | @girs/ 3 | ./results/* 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 3, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Sinomor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import app from "ags/gtk4/app"; 2 | import "@/src/services/styles"; 3 | import request from "./request"; 4 | import { config } from "./options"; 5 | import { windows } from "./windows"; 6 | 7 | app.start({ 8 | icons: `${DATADIR ?? SRC}/assets/icons`, 9 | instanceName: "delta-shell", 10 | main() { 11 | windows(); 12 | }, 13 | requestHandler(argv, response) { 14 | request(argv, response); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /assets/defsong.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinomor/delta-shell/3a51c1a806f7eb45d5ad3f0b681db9982cb18e71/assets/defsong.jpg -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/apps/dark-souls-remastered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinomor/delta-shell/3a51c1a806f7eb45d5ad3f0b681db9982cb18e71/assets/icons/hicolor/scalable/apps/dark-souls-remastered.png -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/apps/umineko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinomor/delta-shell/3a51c1a806f7eb45d5ad3f0b681db9982cb18e71/assets/icons/hicolor/scalable/apps/umineko.png -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/apps/windowkill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sinomor/delta-shell/3a51c1a806f7eb45d5ad3f0b681db9982cb18e71/assets/icons/hicolor/scalable/apps/windowkill.png -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-battery-3-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-battery-4-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 37 | 41 | 42 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-bell-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-bluetooth-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-calendar-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-check-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-chevron-down-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-chevron-left-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-chevron-right-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-chevron-up-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-clock-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-cloud-fog-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-cloud-lightning-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-cloud-moon-rain-symbolic.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-cloud-moon-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-cloud-rain-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-cloud-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-cloudy-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-droplet-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-ethernet-port-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 40 | 44 | 48 | 52 | 56 | 60 | 61 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-log-out-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-map-pin-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-mic-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 40 | 44 | 48 | 52 | 56 | 57 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-moon-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-music-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-pause-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-play-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-power-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-refresh-cw-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 55 | 56 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-search-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-skip-back-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-skip-forward-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-trash-2-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 55 | 59 | 60 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-video-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-volume-1-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-volume-2-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-volume-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-volume-x-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 51 | 52 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-wifi-1-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 40 | 44 | 45 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-wifi-2-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 40 | 44 | 48 | 52 | 56 | 57 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-wifi-3-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 40 | 44 | 48 | 52 | 56 | 57 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-wifi-4-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 40 | 44 | 48 | 52 | 56 | 57 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-wifi-5-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 40 | 44 | 48 | 52 | 56 | 57 | -------------------------------------------------------------------------------- /assets/icons/hicolor/scalable/status/ds-x-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 39 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare const SRC: string; 2 | declare const DATADIR: string | undefined; 3 | 4 | declare module "inline:*" { 5 | const content: string; 6 | export default content; 7 | } 8 | 9 | declare module "*.scss" { 10 | const content: string; 11 | export default content; 12 | } 13 | 14 | declare module "*.blp" { 15 | const content: string; 16 | export default content; 17 | } 18 | 19 | declare module "*.css" { 20 | const content: string; 21 | export default content; 22 | } 23 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'delta-shell', 3 | version: '0.1', 4 | default_options: [ 5 | 'prefix=/usr', 6 | ] 7 | ) 8 | 9 | prefix = get_option('prefix') 10 | bindir = prefix / get_option('bindir') 11 | datadir = prefix / get_option('datadir') / meson.project_name() 12 | 13 | ags = find_program('ags', required: true) 14 | find_program('gjs', required: true) 15 | 16 | custom_target( 17 | input: files('app.ts'), 18 | command: [ 19 | ags, 20 | 'bundle', 21 | '--define', 'DATADIR="' + datadir + '"', 22 | '--root', meson.project_source_root(), 23 | meson.project_source_root() / 'app.ts', 24 | '@OUTPUT@', 25 | ], 26 | output: 'delta-shell-app', 27 | install: true, 28 | install_dir: datadir, 29 | build_always_stale: true, 30 | ) 31 | 32 | configure_file( 33 | input: files('run.sh'), 34 | output: meson.project_name(), 35 | configuration: {'DATADIR': datadir}, 36 | install: true, 37 | install_dir: bindir, 38 | install_mode: 'rwxr-xr-x', 39 | ) 40 | 41 | install_subdir('assets', install_dir: datadir) 42 | install_subdir('src/styles', install_dir: datadir / 'src') 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "typescript": "^5.7.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /request.ts: -------------------------------------------------------------------------------- 1 | import app from "ags/gtk4/app"; 2 | import ScreenRecord from "./src/services/screenrecord"; 3 | import { hide_all_windows, windows_names } from "./windows"; 4 | import { toggleWindow } from "./src/lib/utils"; 5 | import { config } from "./options"; 6 | import { launcher_page_set } from "./src/widgets/launcher/launcher"; 7 | const screenrecord = ScreenRecord.get_default(); 8 | 9 | export default function request( 10 | args: string[], 11 | response: (res: string) => void, 12 | ): void { 13 | if (args[0] == "toggle" && args[1]) { 14 | switch (args[1]) { 15 | case "applauncher": 16 | if (!app.get_window(windows_names.launcher)?.visible) 17 | hide_all_windows(); 18 | launcher_page_set("apps"); 19 | toggleWindow(windows_names.launcher); 20 | break; 21 | case "clipboard": 22 | if (!app.get_window(windows_names.launcher)?.visible) 23 | hide_all_windows(); 24 | launcher_page_set("clipboard"); 25 | toggleWindow(windows_names.launcher); 26 | break; 27 | case "control": 28 | if (!app.get_window(windows_names.control)?.visible) 29 | hide_all_windows(); 30 | toggleWindow(windows_names.control); 31 | break; 32 | case "calendar": 33 | if (!app.get_window(windows_names.calendar)?.visible) 34 | hide_all_windows(); 35 | toggleWindow(windows_names.calendar); 36 | break; 37 | case "powermenu": 38 | if (!app.get_window(windows_names.powermenu)?.visible) 39 | hide_all_windows(); 40 | toggleWindow(windows_names.powermenu); 41 | break; 42 | case "weather": 43 | if (!app.get_window(windows_names.weather)?.visible) 44 | hide_all_windows(); 45 | toggleWindow(windows_names.weather); 46 | break; 47 | case "notifications_list": 48 | if (!app.get_window(windows_names.notifications_list)?.visible) 49 | hide_all_windows(); 50 | toggleWindow(windows_names.notifications_list); 51 | break; 52 | default: 53 | print("Unknown request:", request); 54 | return response("Unknown request"); 55 | break; 56 | } 57 | return response("ok"); 58 | } else { 59 | switch (args[0]) { 60 | case "screenrecord": 61 | screenrecord.start(); 62 | break; 63 | default: 64 | print("Unknown request:", request); 65 | return response("Unknown request"); 66 | break; 67 | } 68 | return response("ok"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /run-dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | cd "$SCRIPT_DIR" 4 | ags run app.ts --define DATADIR=null 5 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WIDGETS=("applauncher" "clipboard" "control" "powermenu" "calendar" "weather" "notifications_list") 4 | 5 | print_help() { 6 | cat < [options] 8 | 9 | Available Commands: 10 | run Run an app 11 | quit Quit an app 12 | restart Restart an app 13 | toggle Toggle visibility of a widget 14 | help Show this help message 15 | 16 | Available widgets for toggle: 17 | $(printf " %s\n" "${WIDGETS[@]}") 18 | 19 | Flags: 20 | -h, --help help for delta-shell 21 | EOF 22 | } 23 | 24 | main() { 25 | if [ $# -lt 1 ]; then 26 | print_help 27 | exit 0 28 | fi 29 | 30 | case $1 in 31 | run) 32 | exec "@DATADIR@/delta-shell-app" 33 | ;; 34 | quit) 35 | exec ags -i delta-shell quit 36 | ;; 37 | restart) 38 | ags -i delta-shell quit 39 | exec "@DATADIR@/delta-shell-app" 40 | ;; 41 | toggle) 42 | if [ "$#" -lt 2 ]; then 43 | echo "Available widgets for toggle:" 44 | printf " - %s\n" "${WIDGETS[@]}" 45 | exit 0 46 | fi 47 | exec ags request -i delta-shell toggle "$2" 48 | ;; 49 | help|-h|--help) print_help ;; 50 | *) 51 | exec ags -i delta-shell request "$*" 52 | ;; 53 | esac 54 | } 55 | 56 | main "$@" 57 | -------------------------------------------------------------------------------- /src/lib/timer.ts: -------------------------------------------------------------------------------- 1 | import { interval } from "ags/time"; 2 | import GLib from "gi://GLib"; 3 | 4 | export class Timer { 5 | private _timeLeft: number; 6 | private _timeout: number; 7 | private _interval: any = null; 8 | private _startTime: number = 0; 9 | private _isPaused: boolean = true; 10 | private subscriptions = new Set<() => void>(); 11 | 12 | constructor(timeout: number) { 13 | this._timeout = timeout; 14 | this._timeLeft = timeout; 15 | } 16 | 17 | get timeLeft(): number { 18 | return this._timeLeft; 19 | } 20 | 21 | set timeLeft(value: number) { 22 | this._timeLeft = value; 23 | this.notify(); 24 | } 25 | 26 | get isPaused(): boolean { 27 | return this._isPaused; 28 | } 29 | 30 | set isPaused(value: boolean) { 31 | if (this._isPaused === value) return; 32 | 33 | this._isPaused = value; 34 | if (value) { 35 | this.pause(); 36 | } else { 37 | this.resume(); 38 | } 39 | } 40 | 41 | notify() { 42 | for (const sub of this.subscriptions) { 43 | sub(); 44 | } 45 | } 46 | 47 | subscribe(callback: () => void): () => void { 48 | this.subscriptions.add(callback); 49 | return () => this.subscriptions.delete(callback); 50 | } 51 | 52 | start() { 53 | this.cancel(); 54 | this._timeLeft = this._timeout; 55 | this._startTime = GLib.get_monotonic_time(); 56 | this._isPaused = false; 57 | 58 | this._interval = interval(100, () => { 59 | if (this._isPaused) return; 60 | 61 | const now = GLib.get_monotonic_time(); 62 | const elapsed = (now - this._startTime) / 1000; 63 | this._timeLeft = Math.max(0, this._timeout - elapsed); 64 | 65 | this.notify(); 66 | 67 | if (this._timeLeft <= 0) { 68 | this.cancel(); 69 | } 70 | }); 71 | } 72 | 73 | pause() { 74 | this._isPaused = true; 75 | } 76 | 77 | resume() { 78 | if (!this._interval || this._timeLeft <= 0) return; 79 | 80 | this._isPaused = false; 81 | this._startTime = 82 | GLib.get_monotonic_time() - (this._timeout - this._timeLeft) * 1000; 83 | } 84 | 85 | cancel() { 86 | if (this._interval) { 87 | this._interval.cancel(); 88 | this._interval = null; 89 | } 90 | this._isPaused = true; 91 | } 92 | 93 | reset() { 94 | this.cancel(); 95 | this._timeLeft = this._timeout; 96 | this.notify(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/services/brightness.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "ags/process"; 2 | import GObject, { register, getter, setter } from "ags/gobject"; 3 | import { monitorFile, readFileAsync } from "ags/file"; 4 | import { bash, dependencies } from "@/src/lib/utils"; 5 | 6 | let screen = ""; 7 | try { 8 | screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`).trim(); 9 | } catch (error) { 10 | console.warn("No backlight devices found"); 11 | } 12 | 13 | const available = dependencies("brightnessctl") && screen !== ""; 14 | 15 | const get = available 16 | ? (args: string) => Number(exec(`brightnessctl ${args}`)) 17 | : () => 0; 18 | 19 | @register({ GTypeName: "Brightness" }) 20 | export default class Brightness extends GObject.Object { 21 | static instance: Brightness; 22 | static get_default() { 23 | if (!this.instance) this.instance = new Brightness(); 24 | return this.instance; 25 | } 26 | 27 | #screenMax = available ? get("max") : 1; 28 | #screen = available ? get("get") / (get("max") || 1) : 0; 29 | #available = available; 30 | 31 | @getter(Number) 32 | get screen() { 33 | return this.#screen; 34 | } 35 | 36 | @getter(Boolean) 37 | get available() { 38 | return this.#available; 39 | } 40 | 41 | @setter(Number) 42 | set screen(percent) { 43 | if (!this.#available) return; 44 | if (percent < 0) percent = 0; 45 | if (percent > 1) percent = 1; 46 | 47 | bash(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => { 48 | this.#screen = percent; 49 | this.notify("screen"); 50 | }); 51 | } 52 | 53 | constructor() { 54 | super(); 55 | 56 | if (this.#available) { 57 | monitorFile(`/sys/class/backlight/${screen}/brightness`, async (f) => { 58 | const v = await readFileAsync(f); 59 | this.#screen = Number(v) / this.#screenMax; 60 | this.notify("screen"); 61 | }); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/services/cliphist.ts: -------------------------------------------------------------------------------- 1 | import GObject, { register, getter } from "ags/gobject"; 2 | import { bash, cacheDir, dependencies, ensureDirectory } from "@/src/lib/utils"; 3 | import { createState } from "ags"; 4 | import { config } from "@/options"; 5 | import { monitorFile } from "ags/file"; 6 | import GLib from "gi://GLib?version=2.0"; 7 | import { subprocess } from "ags/process"; 8 | 9 | @register({ GTypeName: "Cliphist" }) 10 | export default class Cliphist extends GObject.Object { 11 | static instance: Cliphist; 12 | 13 | static get_default() { 14 | if (!this.instance) this.instance = new Cliphist(); 15 | return this.instance; 16 | } 17 | 18 | #list = createState([]); 19 | 20 | constructor() { 21 | super(); 22 | this.start(); 23 | } 24 | 25 | async start() { 26 | if (!dependencies("wl-paste", "cliphist")) return; 27 | 28 | try { 29 | await this.stop(); 30 | 31 | const maxItems = config.launcher.clipboard.max_items.get(); 32 | bash(`wl-paste --watch cliphist -max-items ${maxItems} store`); 33 | monitorFile(`${GLib.get_user_cache_dir()}/cliphist/db`, () => 34 | this.update(), 35 | ); 36 | } catch (error) { 37 | console.error("Failed to start clipboard monitoring:", error); 38 | } 39 | } 40 | 41 | async stop() { 42 | subprocess(`pkill -f "wl-paste.*cliphist"`); 43 | bash(`rm -f ${cacheDir}/cliphist/*.png`); 44 | } 45 | 46 | async update() { 47 | if (!dependencies("cliphist")) return; 48 | 49 | try { 50 | const list = await bash("cliphist list"); 51 | this.#list[1](list.split("\n").filter((line) => line.trim())); 52 | } catch (error) { 53 | console.error("Failed to update clipboard history:", error); 54 | } 55 | } 56 | 57 | async load_image(id: string) { 58 | if (!dependencies("cliphist")) return; 59 | const imagePath = `${cacheDir}/cliphist/${id}.png`; 60 | 61 | try { 62 | ensureDirectory(`${cacheDir}/cliphist`); 63 | await bash(`cliphist decode ${id} > ${imagePath}`); 64 | return imagePath; 65 | } catch (error) { 66 | console.error("Failed to load image preview:", error); 67 | } 68 | } 69 | 70 | async copy(id: string) { 71 | if (!dependencies("cliphist")) return; 72 | try { 73 | return await bash(`cliphist decode ${id} | wl-copy`); 74 | } catch (error) { 75 | console.error("Failed to copy item:", error); 76 | } 77 | } 78 | 79 | async clear() { 80 | if (!dependencies("cliphist")) return; 81 | 82 | try { 83 | await bash("cliphist wipe"); 84 | await this.update(); 85 | } catch (error) { 86 | console.error("Failed to clear clipboard history:", error); 87 | } 88 | } 89 | 90 | get list() { 91 | return this.#list[0]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/services/powermenu.ts: -------------------------------------------------------------------------------- 1 | import { config } from "@/options"; 2 | import { windows_names } from "@/windows"; 3 | import GObject, { getter, property, register, signal } from "ags/gobject"; 4 | import app from "ags/gtk4/app"; 5 | import GLib from "gi://GLib?version=2.0"; 6 | import { Timer } from "../lib/timer"; 7 | import { bash } from "../lib/utils"; 8 | import { timeout } from "ags/time"; 9 | 10 | const user = await GLib.getenv("USER"); 11 | 12 | const commands = { 13 | sleep: "systemctl suspend", 14 | reboot: "systemctl reboot", 15 | logout: `loginctl terminate-user ${user}`, 16 | shutdown: "shutdown now", 17 | }; 18 | 19 | @register({ GTypeName: "Powermenu" }) 20 | export default class Powermenu extends GObject.Object { 21 | static instance: Powermenu; 22 | 23 | static get_default() { 24 | if (!this.instance) this.instance = new Powermenu(); 25 | return this.instance; 26 | } 27 | 28 | constructor() { 29 | super(); 30 | this.#timer.subscribe(async () => { 31 | if (this.#timer.timeLeft <= 0) { 32 | this.executeCommand(); 33 | } 34 | }); 35 | } 36 | 37 | #title = ""; 38 | #label = ""; 39 | #cmd = ""; 40 | #timer = new Timer(60 * 1000); 41 | 42 | @getter(String) 43 | get title() { 44 | return this.#title; 45 | } 46 | 47 | @getter(String) 48 | get label() { 49 | return this.#label; 50 | } 51 | 52 | @getter(String) 53 | get cmd() { 54 | return this.#cmd; 55 | } 56 | 57 | get timer() { 58 | return this.#timer; 59 | } 60 | 61 | async executeCommand() { 62 | this.#timer.cancel(); 63 | await bash(this.#cmd); 64 | app.get_window(windows_names.verification)?.hide(); 65 | } 66 | 67 | cancelAction() { 68 | this.#timer.cancel(); 69 | app.get_window(windows_names.verification)?.hide(); 70 | } 71 | 72 | async action(action: string) { 73 | [this.#cmd, this.#title, this.#label] = { 74 | Sleep: [ 75 | commands.sleep, 76 | "Sleep", 77 | `${user} will be sleep automatically in 60 seconds`, 78 | ], 79 | Reboot: [ 80 | commands.reboot, 81 | "Reboot", 82 | "The system will restart automatically in 60 seconds", 83 | ], 84 | Logout: [ 85 | commands.logout, 86 | "Log Out", 87 | `${user} will be logged out automatically in 60 seconds`, 88 | ], 89 | Shutdown: [ 90 | commands.shutdown, 91 | "Shutdown", 92 | "The system will shutdown automatically in 60 seconds", 93 | ], 94 | }[action]!; 95 | 96 | this.notify("cmd"); 97 | this.notify("title"); 98 | this.notify("label"); 99 | app.get_window(windows_names.powermenu)?.hide(); 100 | app.get_window(windows_names.verification)?.show(); 101 | 102 | this.#timer.reset(); 103 | this.#timer.start(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/services/screenrecord.ts: -------------------------------------------------------------------------------- 1 | import GObject, { register, getter } from "ags/gobject"; 2 | import { 3 | bash, 4 | dependencies, 5 | ensureDirectory, 6 | notifySend, 7 | now, 8 | } from "@/src/lib/utils"; 9 | import GLib from "gi://GLib?version=2.0"; 10 | import { interval, Timer } from "ags/time"; 11 | 12 | const HOME = GLib.get_home_dir(); 13 | 14 | @register({ GTypeName: "Screenrecord" }) 15 | export default class ScreenRecord extends GObject.Object { 16 | static instance: ScreenRecord; 17 | 18 | static get_default() { 19 | if (!this.instance) this.instance = new ScreenRecord(); 20 | return this.instance; 21 | } 22 | 23 | #recordings = `${HOME}/Videos/Screencasting`; 24 | #file = ""; 25 | #interval?: Timer; 26 | #recording = false; 27 | #timer = 0; 28 | 29 | @getter(Boolean) 30 | get recording() { 31 | return this.#recording; 32 | } 33 | 34 | @getter(Number) 35 | get timer() { 36 | return this.#timer; 37 | } 38 | 39 | async start() { 40 | if (!dependencies("gpu-screen-recorder")) return; 41 | if (this.#recording) return; 42 | 43 | ensureDirectory(this.#recordings); 44 | this.#file = `${this.#recordings}/${now()}.mp4`; 45 | 46 | bash( 47 | `gpu-screen-recorder -w screen -f 60 -a default_output -o ${this.#file}`, 48 | ); 49 | 50 | this.#recording = true; 51 | this.notify("recording"); 52 | 53 | this.#timer = 0; 54 | this.#interval = interval(1000, () => { 55 | this.notify("timer"); 56 | this.#timer++; 57 | }); 58 | } 59 | 60 | async stop() { 61 | if (!this.#recording) return; 62 | 63 | await bash("killall -INT gpu-screen-recorder"); 64 | this.#recording = false; 65 | this.notify("recording"); 66 | this.#interval?.cancel(); 67 | 68 | notifySend({ 69 | icon: "folder-videos-symbolic", 70 | appName: "Screen Recorder", 71 | summary: "Screen recording saved", 72 | body: `File saved at ${this.#file}`, 73 | actions: { 74 | "Show in Files": () => bash(`xdg-open ${this.#recordings}`), 75 | View: () => bash(`xdg-open ${this.#file}`), 76 | }, 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/styles/_extra.scss: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: $font-name; 3 | } 4 | 5 | @function all-zero($value) { 6 | $list: if(type-of($value) == list, $value, ($value)); 7 | @each $v in $list { 8 | @if $v != 0 and $v != 0px and $v != "" { 9 | @return false; 10 | } 11 | } 12 | @return true; 13 | } 14 | 15 | @function multiply-padding($values, $multiplier) { 16 | $result: (); 17 | @each $value in $values { 18 | @if $value == 0 { 19 | $result: append($result, 0); 20 | } @else { 21 | $result: append($result, $value * $multiplier); 22 | } 23 | } 24 | @return $result; 25 | } 26 | 27 | @function is-light($color) { 28 | $brightness: (red($color) * 299 + green($color) * 587 + blue($color) * 114) / 29 | 1000; 30 | @return $brightness > 128; 31 | } 32 | 33 | // some hack for fg color with accent 34 | 35 | @function brightness($color) { 36 | @return (red($color) * 299 + green($color) * 587 + blue($color) * 114) / 1000; 37 | } 38 | 39 | @function get-contrast-color($color, $fg, $bg) { 40 | $color-bright: brightness($color); 41 | $fg-bright: brightness($fg); 42 | $bg-bright: brightness($bg); 43 | 44 | $is-light-theme: $bg-bright > 128; 45 | 46 | @if $is-light-theme { 47 | @if $fg-bright < 128 { 48 | @return if($color-bright > 128, $fg, $bg); 49 | } @else { 50 | @return if($color-bright > 128, $bg, $fg); 51 | } 52 | } @else { 53 | @if $fg-bright > 128 { 54 | @return if($color-bright > 128, $bg, $fg); 55 | } @else { 56 | @return if($color-bright > 128, $fg, $bg); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/styles/calendar.scss: -------------------------------------------------------------------------------- 1 | window#calendar { 2 | all: unset; 3 | color: $fg0; 4 | font-size: $font-size; 5 | 6 | .main { 7 | background: rgba($bg0, $window-opacity); 8 | border: $window-border-width solid $window-border-color; 9 | padding: $window-padding; 10 | border-radius: $window-radius; 11 | @if $shadow { 12 | box-shadow: $window-shadow-offset 13 | $window-shadow-blur 14 | $window-shadow-spread 15 | rgba($window-shadow-color, $window-shadow-opacity); 16 | } 17 | } 18 | 19 | .calendar-button, 20 | .weekdays button { 21 | background: transparent; 22 | font-weight: normal; 23 | outline: none; 24 | border-radius: $widget-radius; 25 | padding: 0; 26 | min-width: 45px; 27 | min-height: 45px; 28 | 29 | &:hover { 30 | transition: background $transition; 31 | background: rgba($bg1, $window-opacity); 32 | } 33 | &:focus { 34 | transition: 0s; 35 | outline-offset: -$window-outline-width; 36 | outline: $window-outline-width solid $window-outline-color; 37 | } 38 | } 39 | 40 | .weekend { 41 | color: $red; 42 | } 43 | 44 | .monthshift, 45 | .monthyear { 46 | border-radius: $widget-radius; 47 | background: rgba($bg1, $window-opacity); 48 | font-weight: normal; 49 | outline: none; 50 | padding: 0; 51 | min-height: 45px; 52 | min-width: 45px; 53 | 54 | &:hover { 55 | transition: background $transition; 56 | background: rgba($bg2, $window-opacity); 57 | } 58 | &:focus { 59 | transition: 0s; 60 | outline-offset: -$window-outline-width; 61 | outline: $window-outline-width solid $window-outline-color; 62 | } 63 | } 64 | 65 | .monthyear { 66 | padding: 0 10px; 67 | } 68 | 69 | .today { 70 | background: rgba($accent, $window-opacity); 71 | color: get-contrast-color($accent, $fg0, $bg0); 72 | 73 | &:hover { 74 | color: get-contrast-color($accent-light, $fg0, $bg0); 75 | background: rgba($accent-light, $window-opacity); 76 | } 77 | } 78 | 79 | .other-month { 80 | color: $fg2; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/styles/launcher.scss: -------------------------------------------------------------------------------- 1 | window#launcher { 2 | all: unset; 3 | color: $fg0; 4 | font-size: $font-size; 5 | 6 | .main { 7 | background: rgba($bg0, $window-opacity); 8 | border: $window-border-width solid $window-border-color; 9 | padding: $window-padding; 10 | border-radius: $window-radius; 11 | @if $shadow { 12 | box-shadow: $window-shadow-offset 13 | $window-shadow-blur 14 | $window-shadow-spread 15 | rgba($window-shadow-color, $window-shadow-opacity); 16 | } 17 | } 18 | 19 | .clear { 20 | background: transparent; 21 | padding: 0; 22 | color: $red; 23 | 24 | &:hover { 25 | transition: color $transition; 26 | color: $red-light; 27 | } 28 | 29 | &:focus { 30 | transition: 0s; 31 | outline-offset: -$outline-width; 32 | outline: $outline-width solid $outline-color; 33 | } 34 | } 35 | 36 | .launcher-button { 37 | min-height: 50px; 38 | padding: 0 10px; 39 | border-radius: $widget-radius; 40 | background: transparent; 41 | transition: 0s; 42 | 43 | &:hover { 44 | transition: background 0.3s; 45 | background: rgba($bg1, $window-opacity); 46 | } 47 | 48 | &:focus { 49 | transition: 0s; 50 | outline-offset: -$outline-width; 51 | outline: $outline-width solid $outline-color; 52 | } 53 | 54 | .name, 55 | .id { 56 | font-weight: normal; 57 | } 58 | } 59 | 60 | .image-content { 61 | padding: 10px; 62 | } 63 | 64 | .header { 65 | background: rgba($bg1, $window-opacity); 66 | border-radius: $widget-radius; 67 | padding: 0 10px; 68 | color: $fg0; 69 | 70 | entry { 71 | min-height: 50px; 72 | outline: none; 73 | background: none; 74 | box-shadow: none; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/styles/notifications.scss: -------------------------------------------------------------------------------- 1 | window#notifications_popup { 2 | all: unset; 3 | color: $fg0; 4 | font-size: $font-size; 5 | 6 | .notification { 7 | @if $shadow { 8 | box-shadow: $window-shadow-offset 9 | $window-shadow-blur 10 | $window-shadow-spread 11 | rgba($window-shadow-color, $window-shadow-opacity); 12 | } 13 | } 14 | } 15 | 16 | window#notifications_list { 17 | all: unset; 18 | color: $fg0; 19 | font-size: $font-size; 20 | 21 | .main { 22 | background: rgba($bg0, $window-opacity); 23 | border: $window-border-width solid $window-border-color; 24 | padding: $window-padding; 25 | border-radius: $window-radius; 26 | @if $shadow { 27 | box-shadow: $window-shadow-offset 28 | $window-shadow-blur 29 | $window-shadow-spread 30 | rgba($window-shadow-color, $window-shadow-opacity); 31 | } 32 | } 33 | 34 | tooltip { 35 | background: rgba($bg0, $window-opacity); 36 | border: $border-width solid $border-color; 37 | border-radius: $widget-radius; 38 | margin: 10px; 39 | @if $shadow { 40 | box-shadow: $window-shadow-offset 41 | $window-shadow-blur 42 | $window-shadow-spread 43 | rgba($window-shadow-color, $window-shadow-opacity); 44 | } 45 | } 46 | 47 | .list { 48 | .header { 49 | min-height: 0; 50 | } 51 | } 52 | 53 | .notifs-clear { 54 | outline: none; 55 | padding: 0; 56 | min-height: 40px; 57 | min-width: 40px; 58 | background: transparent; 59 | color: $red; 60 | 61 | &:hover { 62 | transition: all $transition; 63 | color: $red-light; 64 | } 65 | 66 | &:focus { 67 | transition: 0s; 68 | outline-offset: -$outline-width; 69 | outline: $outline-width solid $outline-color; 70 | } 71 | } 72 | .notifs-dnd { 73 | outline: none; 74 | padding: 0; 75 | min-height: 40px; 76 | min-width: 40px; 77 | background: transparent; 78 | 79 | &:focus { 80 | transition: 0s; 81 | outline-offset: -$outline-width; 82 | outline: $outline-width solid $outline-color; 83 | } 84 | } 85 | } 86 | 87 | .notification { 88 | background: rgba($bg1, $window-opacity); 89 | padding: 10px; 90 | border-radius: $widget-radius; 91 | 92 | &.critical { 93 | color: $red; 94 | } 95 | 96 | .header { 97 | .time { 98 | } 99 | 100 | .close { 101 | outline: none; 102 | color: $red; 103 | 104 | &:focus { 105 | transition: 0s; 106 | outline-offset: -$outline-width; 107 | outline: $outline-width solid $outline-color; 108 | } 109 | 110 | &:hover { 111 | color: $red-light; 112 | } 113 | } 114 | 115 | button { 116 | border-radius: $widget-radius; 117 | padding: 0; 118 | background: rgba($bg2, $window-opacity); 119 | } 120 | } 121 | 122 | .content { 123 | .body { 124 | font-style: normal; 125 | font-weight: normal; 126 | } 127 | 128 | .image { 129 | border-radius: $widget-radius; 130 | } 131 | } 132 | 133 | .actions button { 134 | border-radius: $widget-radius; 135 | font-weight: normal; 136 | background: rgba($bg2, $window-opacity); 137 | 138 | &:hover { 139 | background-color: $bg3; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/styles/osd.scss: -------------------------------------------------------------------------------- 1 | window#osd { 2 | all: unset; 3 | color: $fg0; 4 | font-size: $font-size; 5 | 6 | .main { 7 | border-radius: $window-radius; 8 | @if $shadow { 9 | box-shadow: $window-shadow-offset 10 | $window-shadow-blur 11 | $window-shadow-spread 12 | rgba($window-shadow-color, $window-shadow-opacity); 13 | } 14 | } 15 | 16 | .osd-icon { 17 | color: get-contrast-color($accent, $fg0, $bg0); 18 | padding-left: 10px; 19 | &.low { 20 | color: $fg0; 21 | } 22 | } 23 | 24 | levelbar { 25 | trough { 26 | .filled { 27 | border-radius: $widget-radius; 28 | background: rgba($accent, $window-opacity); 29 | } 30 | } 31 | 32 | block { 33 | background: rgba($bg1, $window-opacity); 34 | border-radius: $widget-radius; 35 | min-height: 56px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/styles/powermenu.scss: -------------------------------------------------------------------------------- 1 | window#powermenu { 2 | all: unset; 3 | color: $fg0; 4 | font-size: $font-size; 5 | 6 | .main { 7 | background: rgba($bg0, $window-opacity); 8 | padding: $window-padding; 9 | border: $window-border-width solid $window-border-color; 10 | border-radius: $window-radius; 11 | @if $shadow { 12 | box-shadow: $window-shadow-offset 13 | $window-shadow-blur 14 | $window-shadow-spread 15 | rgba($window-shadow-color, $window-shadow-opacity); 16 | } 17 | } 18 | 19 | .menubutton { 20 | padding: 0; 21 | background: transparent; 22 | font-weight: normal; 23 | outline: none; 24 | 25 | image { 26 | border-radius: $widget-radius; 27 | background: rgba($bg1, $window-opacity); 28 | min-height: 120px; 29 | min-width: 120px; 30 | } 31 | 32 | &:focus { 33 | image { 34 | transition: 0s; 35 | outline-offset: -$window-outline-width; 36 | outline: $window-outline-width solid $window-outline-color; 37 | } 38 | } 39 | 40 | &:hover { 41 | image { 42 | transition: background $transition; 43 | background: rgba($bg2, $window-opacity); 44 | } 45 | } 46 | } 47 | } 48 | 49 | window#verification { 50 | all: unset; 51 | color: $fg0; 52 | 53 | .main { 54 | background: rgba($bg0, $window-opacity); 55 | padding: $window-padding; 56 | border-radius: $window-radius; 57 | border: $border-width solid $border-color; 58 | box-shadow: $window-shadow-offset $window-shadow-blur 59 | $window-shadow-spread 60 | rgba($window-shadow-color, $window-shadow-opacity); 61 | } 62 | 63 | .title { 64 | font-size: 22px; 65 | } 66 | 67 | button { 68 | border-radius: $widget-radius; 69 | font-weight: normal; 70 | outline: none; 71 | padding: 10px; 72 | background: rgba($bg1, $window-opacity); 73 | min-width: 150px; 74 | 75 | &:focus { 76 | transition: 0s; 77 | outline-offset: -$window-outline-width; 78 | outline: $window-outline-width solid $window-outline-color; 79 | } 80 | 81 | &:hover { 82 | transition: background $transition; 83 | background: rgba($bg2, $window-opacity); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/styles/weather.scss: -------------------------------------------------------------------------------- 1 | window#weather { 2 | all: unset; 3 | color: $fg0; 4 | font-size: $font-size; 5 | 6 | .main { 7 | background: rgba($bg0, $window-opacity); 8 | border: $window-border-width solid $window-border-color; 9 | padding: $window-padding; 10 | border-radius: $window-radius; 11 | @if $shadow { 12 | box-shadow: $window-shadow-offset 13 | $window-shadow-blur 14 | $window-shadow-spread 15 | rgba($window-shadow-color, $window-shadow-opacity); 16 | } 17 | } 18 | 19 | .refresh { 20 | background: transparent; 21 | } 22 | 23 | .day, 24 | .hour { 25 | min-width: 50px; 26 | } 27 | 28 | .current { 29 | .temp { 30 | font-size: 40pt; 31 | } 32 | .units { 33 | font-size: 18pt; 34 | } 35 | } 36 | 37 | .forecast { 38 | background: rgba($bg1, $window-opacity); 39 | border-radius: $widget-radius; 40 | padding: 15px; 41 | } 42 | 43 | @keyframes spin { 44 | to { 45 | -gtk-icon-transform: rotate(1turn); 46 | } 47 | } 48 | 49 | .scanning { 50 | &.active { 51 | animation: spin 1s linear infinite; 52 | } 53 | } 54 | 55 | .header { 56 | min-height: 40px; 57 | button { 58 | outline: none; 59 | padding: 0; 60 | min-width: 40px; 61 | background: transparent; 62 | 63 | &:focus { 64 | transition: 0s; 65 | outline-offset: -$outline-width; 66 | outline: $outline-width solid $outline-color; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/widgets/bar/items/clock.tsx: -------------------------------------------------------------------------------- 1 | import app from "ags/gtk4/app"; 2 | import GLib from "gi://GLib"; 3 | import { createPoll } from "ags/time"; 4 | import { onCleanup } from "ags"; 5 | import BarItem from "@/src/widgets/common/baritem"; 6 | import { hide_all_windows, windows_names } from "@/windows"; 7 | import { toggleWindow } from "@/src/lib/utils"; 8 | import { config } from "@/options"; 9 | const { format } = config.bar.date; 10 | 11 | export function Clock() { 12 | const time = createPoll( 13 | "", 14 | 1000, 15 | () => GLib.DateTime.new_now_local().format(format.get())!, 16 | ); 17 | 18 | return ( 19 | { 22 | if (!app.get_window(windows_names.calendar)?.visible) 23 | hide_all_windows(); 24 | toggleWindow(windows_names.calendar); 25 | }} 26 | > 27 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/widgets/bar/items/keyboard.tsx: -------------------------------------------------------------------------------- 1 | import { compositor } from "@/options"; 2 | import { Keyboard_Niri } from "./keyboard/niri"; 3 | import { Keyboard_Hypr } from "./keyboard/hypr"; 4 | import { With } from "ags"; 5 | 6 | export function Keyboard() { 7 | return ( 8 | 9 | 10 | {(comp) => { 11 | if (comp === "niri") return ; 12 | if (comp === "hyprland") return ; 13 | return ; 14 | }} 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/widgets/bar/items/keyboard/hypr.tsx: -------------------------------------------------------------------------------- 1 | import { compositor } from "@/options"; 2 | import { bash } from "@/src/lib/utils"; 3 | import BarItem from "@/src/widgets/common/baritem"; 4 | import { createState, onCleanup } from "ags"; 5 | import AstalHyprland from "gi://AstalHyprland?version=0.1"; 6 | const hyprland = AstalHyprland.get_default(); 7 | 8 | const [layout_name, layout_name_set] = createState("?"); 9 | 10 | function updateLayout() { 11 | bash(`hyprctl devices -j`) 12 | .then((json) => { 13 | try { 14 | const devices = JSON.parse(json); 15 | 16 | const mainKeyboard = devices.keyboards.find( 17 | (kb: any) => kb.main === true, 18 | ); 19 | 20 | if (mainKeyboard && mainKeyboard.active_keymap) { 21 | const layout = mainKeyboard.active_keymap; 22 | 23 | if (layout.includes("English")) { 24 | layout_name_set("En"); 25 | } else if (layout.includes("Russian")) { 26 | layout_name_set("Ru"); 27 | } else { 28 | layout_name_set(layout.substring(0, 2)); 29 | } 30 | } else { 31 | layout_name_set("?"); 32 | } 33 | } catch (error) { 34 | console.error("Failed to parse hyprctl JSON output:", error); 35 | layout_name_set("?"); 36 | } 37 | }) 38 | .catch((err) => { 39 | console.error(`Failed to get keyboard layout: ${err}`); 40 | layout_name_set("?"); 41 | }); 42 | } 43 | 44 | if (compositor.get() === "hyprland") updateLayout(); 45 | 46 | export function Keyboard_Hypr() { 47 | let hyprlandconnect: number; 48 | 49 | onCleanup(() => { 50 | if (hyprlandconnect) hyprland.disconnect(hyprlandconnect); 51 | }); 52 | 53 | return ( 54 | { 56 | try { 57 | const json = await bash(`hyprctl devices -j`); 58 | const devices = JSON.parse(json); 59 | 60 | const mainKeyboard = devices.keyboards.find( 61 | (kb: any) => kb.main === true, 62 | ); 63 | 64 | if (mainKeyboard && mainKeyboard.name) { 65 | bash(`hyprctl switchxkblayout ${mainKeyboard.name} next`); 66 | } 67 | } catch (error) { 68 | console.error("Failed to switch keyboard layout:", error); 69 | } 70 | }} 71 | $={() => { 72 | hyprlandconnect = hyprland.connect( 73 | "keyboard-layout", 74 | (_, kbname, kblayout) => { 75 | updateLayout(); 76 | }, 77 | ); 78 | }} 79 | > 80 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/widgets/bar/items/keyboard/niri.tsx: -------------------------------------------------------------------------------- 1 | import AstalNiri from "gi://AstalNiri"; 2 | import { bash } from "@/src/lib/utils"; 3 | import { createState, onCleanup } from "ags"; 4 | import { compositor } from "@/options"; 5 | import BarItem from "@/src/widgets/common/baritem"; 6 | const niri = AstalNiri.get_default(); 7 | 8 | const [layout_name, layout_name_set] = createState("?"); 9 | 10 | function updateLayout() { 11 | bash(`niri msg keyboard-layouts | grep "*"`) 12 | .then((layout) => { 13 | if (layout.includes("English")) { 14 | layout_name_set("En"); 15 | } else if (layout.includes("Russian")) { 16 | layout_name_set("Ru"); 17 | } else { 18 | layout_name_set("?"); 19 | } 20 | }) 21 | .catch((err) => { 22 | print(`Failed to get keyboard layout: ${err}`); 23 | }); 24 | } 25 | if (compositor.get() === "niri") updateLayout(); 26 | 27 | export function Keyboard_Niri() { 28 | let niriconnect: number; 29 | 30 | onCleanup(() => { 31 | if (niriconnect) niri.disconnect(niriconnect); 32 | }); 33 | 34 | return ( 35 | bash("niri msg action switch-layout next")} 37 | $={() => { 38 | niriconnect = niri.connect("keyboard-layout-switched", () => { 39 | updateLayout(); 40 | }); 41 | }} 42 | > 43 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/widgets/bar/items/launcher.tsx: -------------------------------------------------------------------------------- 1 | import { icons } from "@/src/lib/icons"; 2 | import app from "ags/gtk4/app"; 3 | import { Gdk, Gtk } from "ags/gtk4"; 4 | import { onCleanup } from "ags"; 5 | import BarItem from "@/src/widgets/common/baritem"; 6 | import { hide_all_windows, windows_names } from "@/windows"; 7 | import { toggleWindow } from "@/src/lib/utils"; 8 | import { config } from "@/options"; 9 | import { launcher_page_set } from "@/src/widgets/launcher/launcher"; 10 | 11 | export function Launcher() { 12 | return ( 13 | { 16 | if (!app.get_window(windows_names.launcher)?.visible) 17 | hide_all_windows(); 18 | launcher_page_set("apps"); 19 | toggleWindow(windows_names.launcher); 20 | }} 21 | onSecondaryClick={() => { 22 | if (!app.get_window(windows_names.launcher)?.visible) 23 | hide_all_windows(); 24 | launcher_page_set("clipboard"); 25 | toggleWindow(windows_names.launcher); 26 | }} 27 | > 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/widgets/bar/items/notifications.tsx: -------------------------------------------------------------------------------- 1 | import { config, theme } from "@/options"; 2 | import { icons } from "@/src/lib/icons"; 3 | import { toggleWindow } from "@/src/lib/utils"; 4 | import BarItem from "@/src/widgets/common/baritem"; 5 | import { hide_all_windows, windows_names } from "@/windows"; 6 | import { Gtk } from "ags/gtk4"; 7 | import app from "ags/gtk4/app"; 8 | import AstalNotifd from "gi://AstalNotifd?version=0.1"; 9 | import { createBinding } from "ags"; 10 | const notifd = AstalNotifd.get_default(); 11 | 12 | export function Notifications() { 13 | if (!config.notifications.enabled.get()) return ; 14 | const data = createBinding(notifd, "notifications"); 15 | 16 | return ( 17 | { 20 | if (!app.get_window(windows_names.notifications_list)?.visible) 21 | hide_all_windows(); 22 | toggleWindow(windows_names.notifications_list); 23 | }} 24 | > 25 | 26 | 31 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/widgets/bar/items/recordindicator.tsx: -------------------------------------------------------------------------------- 1 | import { theme } from "@/options"; 2 | import ScreenRecord from "@/src/services/screenrecord"; 3 | import BarItem from "@/src/widgets/common/baritem"; 4 | import { createBinding } from "ags"; 5 | import { Gtk } from "ags/gtk4"; 6 | const screenRecord = ScreenRecord.get_default(); 7 | 8 | export function RecordIndicator() { 9 | return ( 10 | screenRecord.stop().catch(() => "")} 13 | > 14 | 15 | 16 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/widgets/bar/items/sysbox.tsx: -------------------------------------------------------------------------------- 1 | import AstalBattery from "gi://AstalBattery"; 2 | import AstalNetwork from "gi://AstalNetwork"; 3 | import AstalBluetooth from "gi://AstalBluetooth"; 4 | import AstalNotifd from "gi://AstalNotifd?version=0.1"; 5 | import { 6 | icons, 7 | VolumeIcon, 8 | BatteryIcon, 9 | getNetworkIconBinding, 10 | } from "@/src/lib/icons"; 11 | import app from "ags/gtk4/app"; 12 | import { createBinding, createComputed, onCleanup } from "ags"; 13 | import BarItem from "@/src/widgets/common/baritem"; 14 | import Wp from "gi://AstalWp"; 15 | import { Gtk } from "ags/gtk4"; 16 | import { hide_all_windows, windows_names } from "@/windows"; 17 | import { toggleWindow } from "@/src/lib/utils"; 18 | import { config, theme } from "@/options"; 19 | const battery = AstalBattery.get_default(); 20 | const bluetooth = AstalBluetooth.get_default(); 21 | const network = AstalNetwork.get_default(); 22 | const notifd = AstalNotifd.get_default(); 23 | const speaker = Wp.get_default()?.get_default_speaker(); 24 | 25 | export function SysBox() { 26 | const bluetoothconnected = createComputed( 27 | [ 28 | createBinding(bluetooth, "devices"), 29 | createBinding(bluetooth, "isConnected"), 30 | ], 31 | (d, _) => { 32 | for (const device of d) { 33 | if (device.connected) return true; 34 | } 35 | return false; 36 | }, 37 | ); 38 | 39 | return ( 40 | { 43 | if (!app.get_window(windows_names.control)?.visible) 44 | hide_all_windows(); 45 | toggleWindow(windows_names.control); 46 | }} 47 | > 48 | 49 | { 58 | if ( 59 | primary === AstalNetwork.Primary.WIRED && 60 | network.wired.internet === 61 | AstalNetwork.Internet.CONNECTED 62 | ) 63 | return true; 64 | return enabled; 65 | }, 66 | )} 67 | pixelSize={20} 68 | iconName={getNetworkIconBinding()} 69 | /> 70 | 71 | 72 | { 75 | if (dy < 0) speaker.set_volume(speaker.volume + 0.01); 76 | else if (dy > 0) speaker.set_volume(speaker.volume - 0.01); 77 | }} 78 | /> 79 | 80 | 81 | 86 | 87 | 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/widgets/bar/items/tray.tsx: -------------------------------------------------------------------------------- 1 | import AstalTray from "gi://AstalTray?version=0.1"; 2 | import { icons } from "@/src/lib/icons"; 3 | import { Gtk } from "ags/gtk4"; 4 | import { createBinding, createState, For } from "ags"; 5 | import BarItem from "@/src/widgets/common/baritem"; 6 | import { config } from "@/options"; 7 | const tray = AstalTray.get_default(); 8 | 9 | export const Tray = () => { 10 | const [tray_visible, tray_visible_set] = createState(false); 11 | const items = createBinding(tray, "items").as((items) => 12 | items.filter((item) => item.id !== null), 13 | ); 14 | 15 | const init = (btn: Gtk.MenuButton, item: AstalTray.TrayItem) => { 16 | btn.menuModel = item.menuModel; 17 | btn.insert_action_group("dbusmenu", item.actionGroup); 18 | item.connect("notify::action-group", () => { 19 | btn.insert_action_group("dbusmenu", item.actionGroup); 20 | }); 21 | }; 22 | 23 | return ( 24 | 25 | 30 | 31 | 32 | {(item) => ( 33 | init(self, item)}> 34 | 38 | 39 | )} 40 | 41 | 42 | 43 | tray_visible_set((v) => !v)}> 44 | 46 | v ? icons.arrow.right : icons.arrow.left, 47 | )} 48 | pixelSize={20} 49 | /> 50 | 51 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/widgets/bar/items/weather.tsx: -------------------------------------------------------------------------------- 1 | import app from "ags/gtk4/app"; 2 | import GLib from "gi://GLib"; 3 | import { createBinding, createComputed, onCleanup, With } from "ags"; 4 | import BarItem from "@/src/widgets/common/baritem"; 5 | import { Gtk } from "ags/gtk4"; 6 | import { toggleWindow } from "@/src/lib/utils"; 7 | import { hide_all_windows, windows_names } from "@/windows"; 8 | import { config, theme } from "@/options"; 9 | import WeatherService from "@/src/services/weather"; 10 | const weather = WeatherService.get_default(); 11 | 12 | export function Weather() { 13 | if (!config.weather.enabled.get()) return ; 14 | 15 | const data = weather.data.as((data) => { 16 | if (!data) 17 | return { 18 | icon: "", 19 | temp: "", 20 | }; 21 | 22 | const current = data.hourly[0]; 23 | return { 24 | icon: current.icon, 25 | temp: `${current.temperature}${current.units.temperature}`, 26 | }; 27 | }); 28 | return ( 29 | { 32 | if (!app.get_window(windows_names.weather)?.visible) 33 | hide_all_windows(); 34 | toggleWindow(windows_names.weather); 35 | }} 36 | > 37 | d.temp !== "")} 39 | spacing={theme.bar.spacing} 40 | > 41 | d.icon)} 43 | pixelSize={20} 44 | valign={Gtk.Align.CENTER} 45 | /> 46 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/widgets/bar/items/workspaces.tsx: -------------------------------------------------------------------------------- 1 | import { compositor } from "@/options"; 2 | import { Workspaces_Niri } from "./workspaces/niri"; 3 | import { Workspaces_Hypr } from "./workspaces/hypr"; 4 | import { With } from "ags"; 5 | import { Gdk } from "ags/gtk4"; 6 | 7 | export function Workspaces({ gdkmonitor }: { gdkmonitor: Gdk.Monitor }) { 8 | return ( 9 | 10 | 11 | {(comp) => { 12 | if (comp === "niri") 13 | return ; 14 | if (comp === "hyprland") 15 | return ; 16 | return ; 17 | }} 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/widgets/bar/shadow.tsx: -------------------------------------------------------------------------------- 1 | import { Astal, Gdk } from "ags/gtk4"; 2 | import giCairo from "cairo"; 3 | import { createState, onCleanup } from "ags"; 4 | import { config } from "@/options"; 5 | import { windows_names } from "@/windows"; 6 | import app from "ags/gtk4/app"; 7 | 8 | export default function BarShadow({ 9 | gdkmonitor, 10 | $, 11 | }: JSX.IntrinsicElements["window"] & { gdkmonitor: Gdk.Monitor }) { 12 | const { BOTTOM, TOP, LEFT, RIGHT } = Astal.WindowAnchor; 13 | const windows = [ 14 | windows_names.powermenu, 15 | windows_names.verification, 16 | windows_names.calendar, 17 | windows_names.control, 18 | windows_names.launcher, 19 | windows_names.weather, 20 | windows_names.notifications_list, 21 | ]; 22 | const [windowsVisible, windowsVisible_set] = createState([]); 23 | let bar: Astal.Window; 24 | 25 | const appconnect = app.connect("window-toggled", (_, win) => { 26 | const winName = win.name; 27 | if (!windows.includes(winName)) return; 28 | const newVisible = windowsVisible.get(); 29 | 30 | if (win.visible) { 31 | if (!newVisible.includes(winName)) { 32 | newVisible.push(winName); 33 | } 34 | } else { 35 | const index = newVisible.indexOf(winName); 36 | if (index > -1) { 37 | newVisible.splice(index, 1); 38 | } 39 | } 40 | 41 | windowsVisible_set(newVisible); 42 | 43 | bar.set_layer( 44 | newVisible.length > 0 ? Astal.Layer.OVERLAY : Astal.Layer.TOP, 45 | ); 46 | }); 47 | 48 | onCleanup(() => app.disconnect(appconnect)); 49 | 50 | return ( 51 | { 61 | bar = self; 62 | if ($) $(self); 63 | self 64 | .get_native() 65 | ?.get_surface() 66 | ?.set_input_region(new giCairo.Region()); 67 | }} 68 | > 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/widgets/calendar/layout.ts: -------------------------------------------------------------------------------- 1 | function checkLeapYear(year: number) { 2 | return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0); 3 | } 4 | 5 | function getMonthDays(month: number, year: number) { 6 | const leapYear = checkLeapYear(year); 7 | if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) 8 | return 31; 9 | if (month == 2 && leapYear) return 29; 10 | if (month == 2 && !leapYear) return 28; 11 | return 30; 12 | } 13 | 14 | function getNextMonthDays(month: number, year: number) { 15 | const leapYear = checkLeapYear(year); 16 | if (month == 1 && leapYear) return 29; 17 | if (month == 1 && !leapYear) return 28; 18 | if (month == 12) return 31; 19 | if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) 20 | return 30; 21 | return 31; 22 | } 23 | 24 | function getPrevMonthDays(month: number, year: number) { 25 | const leapYear = checkLeapYear(year); 26 | if (month == 3 && leapYear) return 29; 27 | if (month == 3 && !leapYear) return 28; 28 | if (month == 1) return 31; 29 | if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) 30 | return 30; 31 | return 31; 32 | } 33 | 34 | export function getCalendarLayout(dateObject: Date|undefined, highlight: boolean) { 35 | if (!dateObject) dateObject = new Date(); 36 | const weekday = (dateObject.getDay() + 6) % 7; // MONDAY IS THE FIRST DAY OF THE WEEK 37 | const day = dateObject.getDate(); 38 | const month = dateObject.getMonth() + 1; 39 | const year = dateObject.getFullYear(); 40 | const weekdayOfMonthFirst = (weekday + 35 - (day - 1)) % 7; 41 | const daysInMonth = getMonthDays(month, year); 42 | const daysInNextMonth = getNextMonthDays(month, year); 43 | const daysInPrevMonth = getPrevMonthDays(month, year); 44 | 45 | // Fill 46 | let monthDiff = weekdayOfMonthFirst == 0 ? 0 : -1; 47 | let toFill, dim; 48 | if (weekdayOfMonthFirst == 0) { 49 | toFill = 1; 50 | dim = daysInMonth; 51 | } else { 52 | toFill = daysInPrevMonth - (weekdayOfMonthFirst - 1); 53 | dim = daysInPrevMonth; 54 | } 55 | let calendar = [...Array(6)].map(() => Array(7)); 56 | let i = 0, 57 | j = 0; 58 | while (i < 6 && j < 7) { 59 | calendar[i][j] = { 60 | day: toFill, 61 | today: 62 | toFill == day && monthDiff == 0 && highlight 63 | ? 1 64 | : monthDiff == 0 65 | ? 0 66 | : -1, 67 | }; 68 | // Increment 69 | toFill++; 70 | if (toFill > dim) { 71 | // Next month? 72 | monthDiff++; 73 | if (monthDiff == 0) dim = daysInMonth; 74 | else if (monthDiff == 1) dim = daysInNextMonth; 75 | toFill = 1; 76 | } 77 | // Next tile 78 | j++; 79 | if (j == 7) { 80 | j = 0; 81 | i++; 82 | } 83 | } 84 | return calendar; 85 | } 86 | -------------------------------------------------------------------------------- /src/widgets/common/baritem.tsx: -------------------------------------------------------------------------------- 1 | import { Gdk, Gtk } from "ags/gtk4"; 2 | import { CCProps, onCleanup, FCProps } from "ags"; 3 | import app from "ags/gtk4/app"; 4 | 5 | type BarItemProps = JSX.IntrinsicElements["box"] & { 6 | window?: string; 7 | children: any; 8 | onPrimaryClick?: () => void; 9 | onSecondaryClick?: () => void; 10 | onMiddleClick?: () => void; 11 | }; 12 | 13 | export default function BarItem({ 14 | window = "", 15 | children, 16 | onPrimaryClick = () => {}, 17 | onSecondaryClick = () => {}, 18 | onMiddleClick = () => {}, 19 | ...rest 20 | }: BarItemProps) { 21 | return ( 22 | { 25 | if (window) { 26 | const appconnect = app.connect("window-toggled", (_, win) => { 27 | const winName = win.name; 28 | if (winName !== window) return; 29 | const visible = win.visible; 30 | self[visible ? "add_css_class" : "remove_css_class"]( 31 | "active", 32 | ); 33 | }); 34 | onCleanup(() => app.disconnect(appconnect)); 35 | } 36 | }} 37 | {...rest} 38 | > 39 | { 41 | const button = ctrl.get_current_button(); 42 | if (button === Gdk.BUTTON_PRIMARY) { 43 | onPrimaryClick(); 44 | } else if (button === Gdk.BUTTON_SECONDARY) { 45 | onSecondaryClick(); 46 | } else if (button === Gdk.BUTTON_MIDDLE) { 47 | onMiddleClick(); 48 | } 49 | }} 50 | button={0} 51 | /> 52 | {children} 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/widgets/common/baritempopup.tsx: -------------------------------------------------------------------------------- 1 | import { Astal, Gdk, Gtk } from "ags/gtk4"; 2 | import app from "ags/gtk4/app"; 3 | import { Accessor, createComputed, createState } from "ags"; 4 | import { hide_all_windows } from "@/windows"; 5 | import Graphene from "gi://Graphene?version=1.0"; 6 | import Adw from "gi://Adw?version=1"; 7 | import { PopupWindow } from "./popupwindow"; 8 | import { config, theme } from "@/options"; 9 | 10 | type BarItemPopupProps = JSX.IntrinsicElements["window"] & { 11 | children?: any; 12 | module: string; 13 | width?: number; 14 | height?: number; 15 | margin?: number; 16 | gdkmonitor?: Gdk.Monitor; 17 | transitionDuration?: number; 18 | }; 19 | 20 | export function BarItemPopup({ 21 | children, 22 | name, 23 | module, 24 | width, 25 | gdkmonitor, 26 | height, 27 | margin, 28 | transitionDuration = config.transition.get(), 29 | ...props 30 | }: BarItemPopupProps) { 31 | const { bar } = config; 32 | const bar_pos = bar.position.get(); 33 | 34 | const module_pos = createComputed( 35 | [bar.modules.start, bar.modules.center, bar.modules.end], 36 | (start, center, end) => { 37 | if (start.includes(module)) return "start"; 38 | if (center.includes(module)) return "center"; 39 | if (end.includes(module)) return "end"; 40 | }, 41 | ).get(); 42 | 43 | function halign() { 44 | switch (module_pos) { 45 | case "start": 46 | return Gtk.Align.START; 47 | case "center": 48 | return Gtk.Align.CENTER; 49 | case "end": 50 | return Gtk.Align.END; 51 | } 52 | } 53 | function valign() { 54 | switch (bar_pos) { 55 | case "top": 56 | return Gtk.Align.START; 57 | case "bottom": 58 | return Gtk.Align.END; 59 | } 60 | } 61 | 62 | function transitionType() { 63 | return bar_pos === "top" 64 | ? Gtk.RevealerTransitionType.SLIDE_DOWN 65 | : Gtk.RevealerTransitionType.SLIDE_UP; 66 | } 67 | 68 | return ( 69 | 82 | {children} 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/widgets/common/qsbutton.tsx: -------------------------------------------------------------------------------- 1 | import Pango from "gi://Pango"; 2 | import { icons } from "@/src/lib/icons"; 3 | import { Gtk } from "ags/gtk4"; 4 | import { Accessor } from "ags"; 5 | import Adw from "gi://Adw?version=1"; 6 | 7 | type QSButtonProps = { 8 | icon: string | Accessor; 9 | label: string; 10 | subtitle?: Accessor; 11 | showArrow?: boolean; 12 | onClicked: () => void; 13 | onArrowClicked?: () => void; 14 | ButtonClasses: string[] | Accessor; 15 | ArrowClasses?: string[] | Accessor; 16 | maxWidthChars?: number; 17 | }; 18 | 19 | export function QSButton(props: QSButtonProps) { 20 | const { 21 | icon, 22 | label, 23 | subtitle, 24 | onClicked, 25 | showArrow = false, 26 | onArrowClicked = () => {}, 27 | ButtonClasses, 28 | ArrowClasses, 29 | maxWidthChars = 10, 30 | } = props; 31 | 32 | return ( 33 | 34 | 35 | 69 | {showArrow && ( 70 | 77 | )} 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/widgets/control/control.tsx: -------------------------------------------------------------------------------- 1 | import Gtk from "gi://Gtk"; 2 | import { NetworkPage } from "./pages/network"; 3 | import { MainPage } from "./pages/main"; 4 | import { BluetoothPage } from "./pages/bluetooth"; 5 | import { PowerModesPage } from "./pages/powermodes"; 6 | import { createState, onCleanup } from "ags"; 7 | import { hide_all_windows, windows_names } from "@/windows"; 8 | import { PopupWindow } from "../common/popupwindow"; 9 | import { BarItemPopup } from "../common/baritempopup"; 10 | import { config } from "@/options"; 11 | import AstalNetwork from "gi://AstalNetwork?version=0.1"; 12 | import AstalBluetooth from "gi://AstalBluetooth?version=0.1"; 13 | export const [control_page, control_page_set] = createState("main"); 14 | const network = AstalNetwork.get_default(); 15 | const bluetooth = AstalBluetooth.get_default(); 16 | 17 | function Control() { 18 | return ( 19 | { 27 | const unsub = control_page.subscribe(() => 28 | self.set_visible_child_name(control_page.get()), 29 | ); 30 | onCleanup(() => unsub()); 31 | }} 32 | > 33 | 34 | {network.wifi !== null && } 35 | {bluetooth.adapter !== null && } 36 | 37 | 38 | ); 39 | } 40 | 41 | export default function () { 42 | return ( 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/widgets/control/items/sliders.tsx: -------------------------------------------------------------------------------- 1 | import { createBinding } from "ags"; 2 | import { icons, VolumeIcon } from "@/src/lib/icons"; 3 | import { Gtk } from "ags/gtk4"; 4 | import AstalWp from "gi://AstalWp?version=0.1"; 5 | import Brightness from "@/src/services/brightness"; 6 | import { dependencies } from "@/src/lib/utils"; 7 | import { theme } from "@/options"; 8 | const brightness = Brightness.get_default(); 9 | 10 | function BrightnessBox() { 11 | const level = createBinding(brightness, "screen"); 12 | 13 | return ( 14 | `slider-box brightness-box ${v < 0.16 ? "low" : ""}`, 17 | )} 18 | valign={Gtk.Align.CENTER} 19 | > 20 | 27 | { 29 | brightness.screen = value; 30 | }} 31 | hexpand 32 | min={0.1} 33 | value={level} 34 | /> 35 | 36 | ); 37 | } 38 | 39 | function VolumeBox() { 40 | const speaker = AstalWp.get_default()?.audio!.defaultSpeaker!; 41 | const level = createBinding(speaker, "volume"); 42 | 43 | return ( 44 | `slider-box volume-box ${v < 0.05 ? "low" : ""}`, 47 | )} 48 | valign={Gtk.Align.CENTER} 49 | > 50 | 57 | speaker.set_volume(value)} 59 | hexpand 60 | value={level} 61 | /> 62 | 63 | ); 64 | } 65 | 66 | export function Sliders() { 67 | return ( 68 | 73 | 74 | {brightness.available && } 75 | 76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /src/widgets/control/pages/main.tsx: -------------------------------------------------------------------------------- 1 | import { Gtk } from "ags/gtk4"; 2 | import { Sliders } from "../items/sliders"; 3 | import { MprisPlayers } from "../items/media"; 4 | import { Qs_Buttons } from "../items/qsbuttons"; 5 | import { BatteryIcon, icons } from "@/src/lib/icons"; 6 | import AstalBattery from "gi://AstalBattery?version=0.1"; 7 | import app from "ags/gtk4/app"; 8 | import { bash, dependencies, toggleWindow } from "@/src/lib/utils"; 9 | import { createBinding } from "ags"; 10 | import { timeout } from "ags/time"; 11 | import ScreenRecord from "@/src/services/screenrecord"; 12 | import { config, theme } from "@/options"; 13 | import { windows_names } from "@/windows"; 14 | const battery = AstalBattery.get_default(); 15 | const screenRecord = ScreenRecord.get_default(); 16 | 17 | function Power() { 18 | return ( 19 | 30 | ); 31 | } 32 | 33 | function Reload() { 34 | return ( 35 | 46 | ); 47 | } 48 | 49 | function Battery() { 50 | return ( 51 | 65 | ); 66 | } 67 | 68 | export function Header() { 69 | return ( 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | 79 | export function MainPage() { 80 | return ( 81 | 88 |
89 | 90 | 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/widgets/control/pages/powermodes.tsx: -------------------------------------------------------------------------------- 1 | import { icons } from "@/src/lib/icons"; 2 | import { Gtk } from "ags/gtk4"; 3 | import AstalPowerProfiles from "gi://AstalPowerProfiles?version=0.1"; 4 | import { createBinding } from "ags"; 5 | import { theme } from "@/options"; 6 | import { control_page_set } from "../control"; 7 | 8 | const power = AstalPowerProfiles.get_default(); 9 | 10 | function Header() { 11 | return ( 12 | 13 | 20 |