├── .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 |
42 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-battery-4-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
42 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-bell-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-bluetooth-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-calendar-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-check-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-chevron-down-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-chevron-left-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-chevron-right-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-chevron-up-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-clock-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-cloud-fog-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
52 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-cloud-lightning-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-cloud-moon-rain-symbolic.svg:
--------------------------------------------------------------------------------
1 |
17 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-cloud-moon-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-cloud-rain-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-cloud-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-cloudy-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-droplet-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-ethernet-port-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
61 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-log-out-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
52 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-map-pin-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-mic-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-moon-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-music-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
52 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-pause-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-play-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-power-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-refresh-cw-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
56 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-search-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-skip-back-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-skip-forward-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-trash-2-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
60 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-video-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-volume-1-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
48 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-volume-2-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
52 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-volume-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-volume-x-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
52 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-wifi-1-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
45 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-wifi-2-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-wifi-3-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-wifi-4-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-wifi-5-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/assets/icons/hicolor/scalable/status/ds-x-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
81 |
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 |
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 |
27 | );
28 | }
29 |
30 | export const profiles_names = {
31 | "power-saver": "Power Saver",
32 | balanced: "Balanced",
33 | performance: "Performance",
34 | } as Record;
35 |
36 | function Item({ profile }: { profile: string }) {
37 | const isConnected = createBinding(power, "activeProfile").as(
38 | (p) => p === profile,
39 | );
40 |
41 | function setProfile(profile: string) {
42 | power.set_active_profile(profile);
43 | }
44 |
45 | return (
46 |
62 | );
63 | }
64 |
65 | function List() {
66 | const list = power.get_profiles();
67 |
68 | return (
69 |
70 |
75 | {list.map(({ profile }) => (
76 |
77 | ))}
78 |
79 |
80 | );
81 | }
82 |
83 | export function PowerModesPage() {
84 | return (
85 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/src/widgets/launcher/items/app_button.tsx:
--------------------------------------------------------------------------------
1 | import { Gtk } from "ags/gtk4";
2 | import AstalApps from "gi://AstalApps?version=0.1";
3 | import Pango from "gi://Pango?version=1.0";
4 | import { hide_all_windows } from "@/windows";
5 |
6 | export function AppButton({ app }: { app: AstalApps.Application }) {
7 | return (
8 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/widgets/launcher/items/clip_color.tsx:
--------------------------------------------------------------------------------
1 | import Pango from "gi://Pango?version=1.0";
2 | import { bash } from "@/src/lib/utils";
3 | import { Gdk, Gtk } from "ags/gtk4";
4 | import { hide_all_windows } from "@/windows";
5 | import Cliphist from "@/src/services/cliphist";
6 | const clipboard = Cliphist.get_default();
7 |
8 | export function ClipColor({ id, content }: { id: string; content: string }) {
9 | const gdkColor = new Gdk.RGBA();
10 | const isValid = gdkColor.parse(content);
11 |
12 | return (
13 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/widgets/launcher/items/clip_image.tsx:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio?version=2.0";
2 | import { bash, ensureDirectory } from "@/src/lib/utils";
3 | import { Gtk } from "ags/gtk4";
4 | import { timeout } from "ags/time";
5 | import app from "ags/gtk4/app";
6 | import { createState, onCleanup } from "ags";
7 | import { hide_all_windows, windows_names } from "@/windows";
8 | import { config, theme } from "@/options";
9 | import Cliphist from "@/src/services/cliphist";
10 | const clipboard = Cliphist.get_default();
11 |
12 | export function ClipImage({
13 | id,
14 | content,
15 | }: {
16 | id: string;
17 | content: RegExpMatchArray;
18 | }) {
19 | const [_, size, unit, format, width, height] = content;
20 | const maxWidth =
21 | config.launcher.width.get() - theme.window.padding.get() * 2;
22 | let widthPx = (Number(width) / Number(height)) * 200;
23 | let heightPx: number;
24 |
25 | if (widthPx > maxWidth) heightPx = (200 / widthPx) * maxWidth;
26 | else heightPx = 200;
27 |
28 | return (
29 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/widgets/launcher/items/clip_text.tsx:
--------------------------------------------------------------------------------
1 | import Pango from "gi://Pango?version=1.0";
2 | import { bash } from "@/src/lib/utils";
3 | import { Gtk } from "ags/gtk4";
4 | import { hide_all_windows } from "@/windows";
5 | import Cliphist from "@/src/services/cliphist";
6 | const clipboard = Cliphist.get_default();
7 |
8 | export function ClipText({ id, content }: { id: string; content: string }) {
9 | return (
10 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/widgets/launcher/launcher.tsx:
--------------------------------------------------------------------------------
1 | import Gtk from "gi://Gtk";
2 | import { AppLauncher } from "./pages/applauncher";
3 | import { Clipboard } from "./pages/clipboard";
4 | import { hide_all_windows, windows_names } from "@/windows";
5 | import Adw from "gi://Adw?version=1";
6 | import { PopupWindow } from "../common/popupwindow";
7 | import { createState, onCleanup } from "ags";
8 | import { BarItemPopup } from "../common/baritempopup";
9 | import { config } from "@/options";
10 | const { width, height } = config.launcher;
11 | export const [launcher_page, launcher_page_set] = createState("apps");
12 |
13 | function Launcher() {
14 | return (
15 | {
21 | const unsub = launcher_page.subscribe(() =>
22 | self.set_visible_child_name(launcher_page.get()),
23 | );
24 | onCleanup(() => unsub());
25 | }}
26 | >
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | export default function () {
34 | return (
35 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/widgets/launcher/pages/applauncher.tsx:
--------------------------------------------------------------------------------
1 | import app from "ags/gtk4/app";
2 | import Apps from "gi://AstalApps?version=0.1";
3 | import { AppButton } from "../items/app_button";
4 | import { Gtk } from "ags/gtk4";
5 | import { createComputed, createState, For, onCleanup } from "ags";
6 | import { hide_all_windows, windows_names } from "@/windows";
7 | import { config, theme } from "@/options";
8 | import { launcher_page } from "../launcher";
9 |
10 | const apps = new Apps.Apps();
11 | const [text, text_set] = createState("");
12 | let scrolled: Gtk.ScrolledWindow;
13 | const list = text.as((text) => {
14 | return apps.fuzzy_query(text);
15 | });
16 |
17 | function Entry() {
18 | let appconnect: number;
19 |
20 | onCleanup(() => {
21 | if (appconnect) app.disconnect(appconnect);
22 | });
23 |
24 | const onEnter = () => {
25 | list.get()[0].launch();
26 | hide_all_windows();
27 | };
28 |
29 | return (
30 | {
33 | appconnect = app.connect("window-toggled", async (_, win) => {
34 | const winName = win.name;
35 | const visible = win.visible;
36 | const mode = launcher_page.get() == "apps";
37 |
38 | if (winName == windows_names.launcher && visible && mode) {
39 | scrolled.set_vadjustment(null);
40 | await apps.reload();
41 | text_set("");
42 | self.set_text("");
43 | self.grab_focus();
44 | }
45 | });
46 | }}
47 | placeholderText={"Search..."}
48 | onActivate={() => onEnter()}
49 | onNotifyText={(self) => {
50 | scrolled.set_vadjustment(null);
51 | text_set(self.text);
52 | }}
53 | />
54 | );
55 | }
56 |
57 | function Header() {
58 | return (
59 |
62 | );
63 | }
64 |
65 | function List() {
66 | return (
67 | (scrolled = self)}>
68 |
73 | {(app) => }
74 |
75 |
76 | );
77 | }
78 |
79 | function NotFound() {
80 | return (
81 | l.length === 0)}
87 | >
88 |
89 |
90 | );
91 | }
92 |
93 | export function AppLauncher() {
94 | return (
95 |
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/src/widgets/powermenu/powermenu.tsx:
--------------------------------------------------------------------------------
1 | import Gtk from "gi://Gtk";
2 | import { icons } from "@/src/lib/icons";
3 | import Powermenu from "@/src/services/powermenu";
4 | import { hide_all_windows, windows_names } from "@/windows";
5 | import { PopupWindow } from "../common/popupwindow";
6 | import { config, theme } from "@/options";
7 | const powermenu = Powermenu.get_default();
8 |
9 | type MenuButtonProps = {
10 | icon: string;
11 | label: string;
12 | clicked: () => void;
13 | };
14 |
15 | function MenuButton({ icon, label, clicked }: MenuButtonProps) {
16 | return (
17 |
28 | );
29 | }
30 |
31 | const list = ["Sleep", "Logout", "Reboot", "Shutdown"];
32 |
33 | function PowerMenu() {
34 | return (
35 |
36 | {list.map((value) => (
37 | powermenu.action(value)}
41 | />
42 | ))}
43 |
44 | );
45 | }
46 |
47 | export default function () {
48 | return (
49 |
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/widgets/powermenu/verification.tsx:
--------------------------------------------------------------------------------
1 | import Gtk from "gi://Gtk";
2 | import Powermenu from "@/src/services/powermenu";
3 | import { createBinding } from "ags";
4 | import { hide_all_windows, windows_names } from "@/windows";
5 | import { PopupWindow } from "../common/popupwindow";
6 | import { config, theme } from "@/options";
7 | import { bash } from "@/src/lib/utils";
8 | import Pango from "gi://Pango?version=1.0";
9 | import Adw from "gi://Adw?version=1";
10 | import app from "ags/gtk4/app";
11 | const powermenu = Powermenu.get_default();
12 |
13 | function Verification() {
14 | return (
15 |
16 |
17 |
18 |
25 |
26 |
27 |
44 |
45 | );
46 | }
47 |
48 | export default function () {
49 | const appconnect = app.connect("window-toggled", (_, win) => {
50 | const winName = win.name;
51 | const visible = win.visible;
52 |
53 | if (winName == windows_names.verification && !visible) {
54 | powermenu.cancelAction();
55 | }
56 | });
57 |
58 | return (
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/widgets/weather/items/current.tsx:
--------------------------------------------------------------------------------
1 | import { Gtk } from "ags/gtk4";
2 | import { With } from "ags";
3 | import WeatherService from "@/src/services/weather";
4 | const weather = WeatherService.get_default();
5 |
6 | function getDescription(weatherCode: number) {
7 | const descriptions = {
8 | 0: "Clear sky",
9 | 1: "Mainly clear",
10 | 2: "Partly cloudy",
11 | 3: "Overcast",
12 | 45: "Fog",
13 | 48: "Depositing rime fog",
14 | 51: "Light drizzle",
15 | 53: "Moderate drizzle",
16 | 55: "Dense drizzle",
17 | 56: "Light freezing drizzle",
18 | 57: "Dense freezing drizzle",
19 | 61: "Slight rain",
20 | 63: "Moderate rain",
21 | 65: "Heavy rain",
22 | 66: "Light freezing rain",
23 | 67: "Heavy freezing rain",
24 | 71: "Slight snow fall",
25 | 73: "Moderate snow fall",
26 | 75: "Heavy snow fall",
27 | 77: "Snow grains",
28 | 80: "Slight rains showers",
29 | 81: "Moderate rain showers",
30 | 82: "Violent rain showers",
31 | 85: "Slight snow nshowers",
32 | 86: "Heavy snow showers",
33 | 95: "Thunderstorm",
34 | 96: "Thunderstorm with slight hail",
35 | 99: "Thunderstorm with heavy hail",
36 | } as Record;
37 |
38 | return descriptions[weatherCode];
39 | }
40 |
41 | export function Current() {
42 | const data = weather.data.as((data) => {
43 | if (!data)
44 | return {
45 | feels: "",
46 | temp: "",
47 | units: "",
48 | desc: "",
49 | };
50 |
51 | const current = data.hourly[0];
52 | return {
53 | feels: `Feels like ${current.apparent_temperature}${current.units.temperature}`,
54 | temp: `${current.temperature}`,
55 | units: `${current.units.temperature}`,
56 | desc: getDescription(current.weather_code),
57 | };
58 | });
59 |
60 | return (
61 |
62 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/src/widgets/weather/items/days.tsx:
--------------------------------------------------------------------------------
1 | import { Gtk } from "ags/gtk4";
2 | import { DailyWeather } from "@/src/services/weather";
3 | import { For } from "ags";
4 | import { icons } from "@/src/lib/icons";
5 | import { theme } from "@/options";
6 | import WeatherService from "@/src/services/weather";
7 | const weather = WeatherService.get_default();
8 |
9 | function formatDate(timestamp: number): string {
10 | const date = new Date(timestamp * 1000);
11 | return date.toLocaleDateString([], {
12 | day: "2-digit",
13 | month: "2-digit",
14 | });
15 | }
16 |
17 | function formateWeekDay(timestamp: number): string {
18 | const date = new Date(timestamp * 1000);
19 | const today = new Date();
20 | const weekday = date.toLocaleDateString([], {
21 | weekday: "short",
22 | });
23 | if (
24 | date.getDate() === today.getDate() &&
25 | date.getMonth() === today.getMonth() &&
26 | date.getFullYear() === today.getFullYear()
27 | )
28 | return "Today";
29 | else return weekday;
30 | }
31 |
32 | function Day({ day }: { day: DailyWeather }) {
33 | return (
34 |
39 |
40 |
41 |
42 |
45 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export function Days() {
58 | const days = weather.data.as((data) => {
59 | if (!data) return [];
60 | return data?.daily;
61 | });
62 |
63 | return (
64 |
69 |
70 |
71 |
72 |
73 |
77 |
78 | {(day) => }
79 |
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/src/widgets/weather/items/hours.tsx:
--------------------------------------------------------------------------------
1 | import { Gtk } from "ags/gtk4";
2 | import { For } from "ags";
3 | import { icons } from "@/src/lib/icons";
4 | import { theme } from "@/options";
5 | import { HourlyWeather } from "@/src/services/weather";
6 | import WeatherService from "@/src/services/weather";
7 | const weather = WeatherService.get_default();
8 |
9 | function formatHour(timestamp: number): string {
10 | const date = new Date(timestamp * 1000);
11 | const now = new Date();
12 | const hour = date.toLocaleTimeString([], {
13 | hour12: false,
14 | hour: "2-digit",
15 | minute: "2-digit",
16 | });
17 | if (date.getHours() === now.getHours()) return "Now";
18 | else return hour;
19 | }
20 |
21 | function Hour({ hour }: { hour: HourlyWeather }) {
22 | return (
23 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | export function Hours() {
40 | const hours = weather.data.as((data) => {
41 | if (!data) return [];
42 | return data?.hourly;
43 | });
44 |
45 | return (
46 |
51 |
52 |
53 |
54 |
55 |
59 |
60 | {(hour) => }
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/widgets/weather/weather.tsx:
--------------------------------------------------------------------------------
1 | import Gtk from "gi://Gtk";
2 | import { hide_all_windows, windows_names } from "@/windows";
3 | import { BarItemPopup } from "../common/baritempopup";
4 | import { createComputed } from "ags";
5 | import { Current } from "./items/current";
6 | import { icons } from "@/src/lib/icons";
7 | import { Days } from "./items/days";
8 | import { Hours } from "./items/hours";
9 | import { config, theme } from "@/options";
10 | import WeatherService from "@/src/services/weather";
11 | const weather = WeatherService.get_default();
12 |
13 | function ScanningIndicator() {
14 | const className = weather.loading.as((scanning) => {
15 | const classes = ["scanning"];
16 | if (scanning) {
17 | classes.push("active");
18 | }
19 | return classes;
20 | });
21 |
22 | return (
23 |
24 | );
25 | }
26 |
27 | function Header() {
28 | const data = weather.location.as((location) => {
29 | if (!location)
30 | return {
31 | label: "",
32 | };
33 |
34 | return {
35 | label: `${location.city}, ${location.country_code}`,
36 | };
37 | });
38 |
39 | return (
40 |
52 | );
53 | }
54 |
55 | function Weather() {
56 | return (
57 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
71 | export default function () {
72 | return (
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "ags/gtk4",
6 | "lib": [
7 | "ES2023"
8 | ],
9 | "module": "ES2022",
10 | "moduleResolution": "Bundler",
11 | "paths": {
12 | "@/*": [
13 | "./*"
14 | ]
15 | },
16 | "strict": true,
17 | "target": "ES2020"
18 | }
19 | }
--------------------------------------------------------------------------------
/windows.tsx:
--------------------------------------------------------------------------------
1 | import Bar from "./src/widgets/bar/bar";
2 | import OSD from "./src/widgets/osd/osd";
3 | import Launcher from "./src/widgets/launcher/launcher";
4 | import Control, { control_page_set } from "./src/widgets/control/control";
5 | import Calendar from "./src/widgets/calendar/calendar";
6 | import Powermenu from "./src/widgets/powermenu/powermenu";
7 | import Verification from "./src/widgets/powermenu/verification";
8 | import { NotificationPopup } from "./src/widgets/notifications/notificationpopup";
9 | import app from "ags/gtk4/app";
10 | import Weather from "./src/widgets/weather/weather";
11 | import BarShadow from "./src/widgets/bar/shadow";
12 | import { config, theme } from "./options";
13 | import NotificationsList from "./src/widgets/notifications/notificationslist";
14 | import { createBinding, For, onCleanup, This } from "ags";
15 | import { Gtk } from "ags/gtk4";
16 |
17 | export const windows_names = {
18 | bar: "bar",
19 | bar_shadow: "bar_shadow",
20 | launcher: "launcher",
21 | notifications_popup: "notifications_popup",
22 | control: "control",
23 | osd: "osd",
24 | powermenu: "powermenu",
25 | verification: "verification",
26 | weather: "weather",
27 | calendar: "calendar",
28 | notifications_list: "notifications_list",
29 | };
30 |
31 | export function hide_all_windows() {
32 | app.get_window(windows_names.launcher)?.hide();
33 | app.get_window(windows_names.powermenu)?.hide();
34 | app.get_window(windows_names.verification)?.hide();
35 | app.get_window(windows_names.calendar)?.hide();
36 | app.get_window(windows_names.control)?.hide();
37 | config.weather.enabled.get() &&
38 | app.get_window(windows_names.weather)?.hide();
39 | config.notifications.enabled.get() &&
40 | app.get_window(windows_names.notifications_list)?.hide();
41 | control_page_set("main");
42 | }
43 |
44 | export function windows() {
45 | Launcher();
46 | Control();
47 | Calendar();
48 | Powermenu();
49 | Verification();
50 | if (config.weather.enabled.get()) Weather();
51 | if (config.notifications.enabled.get()) {
52 | NotificationsList();
53 | NotificationPopup();
54 | }
55 | OSD();
56 |
57 | const monitors = createBinding(app, "monitors");
58 |
59 |
60 | {(monitor) => (
61 |
62 | onCleanup(() => self.destroy())}
65 | />
66 | {theme.shadow.get() && (
67 | onCleanup(() => self.destroy())}
70 | />
71 | )}
72 |
73 | )}
74 | ;
75 | }
76 |
--------------------------------------------------------------------------------