426 Commits

Author SHA1 Message Date
janishutz 75a9e6d953 feat: add current gtk config 2026-06-15 09:31:06 +02:00
janishutz 85f3db3616 fix: add libastal meta package to install 2026-06-15 09:19:13 +02:00
janishutz fc51bb593a fix: trailing commas, explicitly installed pkgs list 2026-06-13 18:08:26 +02:00
janishutz 14ef414781 Update README.md 2026-06-13 15:09:31 +02:00
janishutz 205e667343 feat(hypr): New window rules 2026-06-11 13:12:51 +02:00
janishutz b7e431b3fa feat(hypr): improve muting bind 2026-06-07 14:34:10 +02:00
janishutz 9602c673e5 [Hypr] move lua setup to new folder, add bind to toggle muting of default mic 2026-05-30 10:40:54 +02:00
janishutz e73e7b2353 feat: fix eslint config 2026-05-30 10:14:24 +02:00
janishutz 820e4c9982 [Hypr] Start new hyprland config 2026-05-12 11:43:42 +02:00
janishutz 4b9bbde225 [Hyprland] Temporary fix before full config rewrite in lua 2026-05-11 12:31:40 +02:00
janishutz 82f375794b [Yazi] Use package manager for themes 2026-05-11 12:03:06 +02:00
janishutz a87f908b4d [Hyrpland] Improve bind 2026-05-09 14:39:42 +02:00
janishutz e44f8728a8 [Hypridle] Improve memory footprint 2026-04-29 11:50:07 +02:00
janishutz bb737e524e [Build] Change wallpaper 2026-04-23 16:45:28 +02:00
janishutz fe14b2c78f [Astal] Notifications: Delete not working version 2026-04-23 10:45:03 +02:00
janishutz 50012b04bc [Hyprland] Add windowrule for steam games 2026-04-09 15:16:48 +02:00
janishutz 5e8f2e8930 [Hyprland] Speed up animations 2026-03-25 14:49:33 +01:00
janishutz af675c0683 [Hyprland] Improve touchpad binds 2026-03-25 12:24:26 +01:00
janishutz e03eb2ddd9 [Hyprland] Add more dwindle binds 2026-03-18 16:06:05 +01:00
janishutz 9816e79eec [Hyprland] Add binds to disable trackpad 2026-03-18 11:09:58 +01:00
janishutz 3600c5bcd3 [Fish] Add git pull rebase alias 2026-03-12 17:39:25 +01:00
janishutz 42800f5803 [Hyprland] Default to 120hz on laptop 2026-03-12 11:21:16 +01:00
janishutz bd46b4d609 [Fish] Add alias for commonly accidentally run command 2026-03-11 18:08:16 +01:00
janishutz 79e23782cc [Hyprland] Add windowrules for LÖVR 2026-03-11 16:50:01 +01:00
janishutz aec0f4efd1 [Yazi] Add better keymap for eth stuff 2026-03-11 14:16:10 +01:00
janishutz 35cc75ee2e Revert "[Hyprland] Move to qalculate-gtk from qt"
This reverts commit 53d90fc8f5.
2026-02-25 17:37:26 +01:00
janishutz 53d90fc8f5 [Hyprland] Move to qalculate-gtk from qt 2026-02-25 17:35:16 +01:00
janishutz 5f1f69a6db [Build] Use other wallpaper 2026-02-25 09:38:59 +01:00
janishutz c37a641060 [Yazi] Add new keybind 2026-02-23 19:09:46 +01:00
janishutz d6a6d62ad6 [Yazi] Finish new semester update 2026-02-20 16:37:45 +01:00
janishutz 01ed1dbdda [Fish] Update to 4.3 fish 2026-02-20 12:35:33 +01:00
janishutz 2ac5a4af14 [Yazi] Add new semester binds 2026-02-17 15:33:15 +01:00
janishutz e7b418007f [Yazi] Keybinds 2026-02-17 13:58:02 +01:00
janishutz 0b896e404c [Hyprland] Windowrules 2026-02-17 13:57:55 +01:00
janishutz c38907ec39 [Hyprland] add launch command for qutebrowser 2026-02-02 09:07:09 +01:00
janishutz 1721651319 [Hyprland] Add binds for qalculate 2026-01-23 13:41:07 +01:00
janishutz e4692c456c [Hyprland] Make steam not start in floating mode 2026-01-22 19:48:26 +01:00
janishutz 6ee881c7dc [Hyprland] Update for new keyboard having native ability to swap caps and escape 2026-01-19 11:08:22 +01:00
janishutz 2649ee4b56 [Hyprland] Remove hyprpm reload 2026-01-09 17:41:47 +01:00
janishutz 4024b9e830 [Setup] Improve color extraction 2026-01-09 16:23:30 +01:00
janishutz 11767c3009 [Astal] Prepare for reloadable style 2026-01-09 16:04:51 +01:00
janishutz c2cd216dac [Scripts] Another incorrect filename fix 2026-01-09 15:04:45 +01:00
janishutz 5c245f9c35 [Scripts] Fix incorrect filename 2026-01-09 14:46:04 +01:00
janishutz 54fb71eeae [Scripts] Add script to add latexindent 2026-01-09 14:45:09 +01:00
janishutz 44642960ad [Hyprland] Remove vivado shenanigans 2026-01-09 07:45:59 +01:00
janishutz f8b7f00f44 [Hyprland] Flat accel profile 2026-01-06 14:03:59 +01:00
janishutz 069139641f [Yazi] Update to new conf 2026-01-06 13:03:33 +01:00
janishutz 17689ac0e8 [Hyprland] Update config to newest version 2026-01-01 15:10:39 +01:00
janishutz 5581267a73 [Fish] Add alias for flutter via fvm 2026-01-01 12:50:09 +01:00
janishutz e2fd60187a [Hyprland] Improve scrolling 2025-12-19 11:08:09 +01:00
janishutz 37ff1af0ed [Kitty] Improve scrolling 2025-12-19 11:04:12 +01:00
janishutz 37849dee8f [Hyprland] Finish change to us-intl layout 2025-12-14 11:05:19 +01:00
janishutz fe36f97774 [Eslint] More fixes 2025-12-13 14:29:12 +01:00
janishutz e9399cf7bb [Eslint] Fix typing 2025-12-13 14:28:22 +01:00
janishutz c453e7aa90 [Hyprland] improve notifications 2025-12-09 11:26:04 +01:00
janishutz 5c069beb2b [Hyprland] Add notifications to layout change 2025-12-09 11:24:07 +01:00
janishutz 0a555e9e5a [Hyprland] update bind 2025-12-09 11:22:52 +01:00
janishutz c7e82042af [Hyprland] update keyboard layout and add layout change binds 2025-12-09 11:17:07 +01:00
janishutz c421ea2cb3 [Hyprland] Fix binds 2025-12-08 10:29:08 +01:00
janishutz 84b008e29c [Hyprland] Extract gestures into separate file 2025-12-08 10:28:11 +01:00
janishutz 007c3d0bf0 [Eslint] Fix config error 2025-12-04 14:50:46 +01:00
janishutz 8872abecbd [Eslint] Update config 2025-12-04 11:22:01 +01:00
janishutz 841575d4e3 [Fish] Update alias 2025-11-27 16:46:14 +01:00
janishutz befe9696b0 [Fish] Fix alias 2025-11-27 16:45:38 +01:00
janishutz 4b12ae2c12 [Scripts] Fixes for vpn script 2025-11-24 14:25:13 +01:00
janishutz 5227e5f05e [Scripts] Fix vpn script 2025-11-23 13:36:06 +01:00
janishutz b8098eac8b [Scripts] Improve VPN script 2025-11-18 09:38:24 +01:00
janishutz 5b610643c7 [Install] Fix bt 2025-11-17 09:17:27 +01:00
janishutz 4fce8c955c [Hyprland] Fix cursor size 2025-11-12 10:32:15 +01:00
janishutz eabb972cb8 [Binds] Fix swapescape 2025-11-11 09:58:48 +01:00
janishutz 688b94c3ad [Binds] Add binds to dynamically enable and disable caps:swapescape 2025-11-11 09:50:31 +01:00
janishutz d4689d66cd [FMT] Improve latexindent config 2025-11-11 08:05:53 +01:00
janishutz e28e3e6207 [Install] Add usbutils 2025-11-06 17:47:18 +01:00
janishutz b45b3d60d1 [Scripts] Add pdf wordcount script 2025-11-04 15:51:49 +01:00
janishutz 8d2e3cea85 [Hyprland] Move conflicting bind 2025-11-03 17:13:44 +01:00
janishutz 8a42f9bfa2 [Install] Add timesyncd enable 2025-11-03 10:15:00 +01:00
janishutz 09916ae68e [Hyprland] Add more launch commands 2025-11-02 10:49:33 +01:00
janishutz 5b5fa9ad4e [Astal] Fix launch on new astal version 2025-11-01 10:37:30 +01:00
janishutz 814b975533 [Linecount] Add cloc and alias for it 2025-10-30 15:31:47 +01:00
janishutz d0a450d4e8 [Setup] Make cleaner 2025-10-30 10:42:50 +01:00
janishutz 786d5c5fe0 [Yazi] Fix themes 2025-10-30 10:42:42 +01:00
janishutz 5ba2894101 [Hyprland] Improve windowrules 2025-10-30 10:24:08 +01:00
janishutz 499bf793b2 [Hyprland] Add some nice windorules 2025-10-30 10:24:08 +01:00
janishutz f1c57b8ecc [Scripts] Add nvim update script 2025-10-29 15:19:18 +01:00
janishutz 20cc042b64 [Scripts] Add batch video conversion script 2025-10-25 16:26:39 +02:00
janishutz ae9dcaa074 [Hyprland] Windowrules: Add for BeatSaber 2025-10-22 17:01:28 +02:00
janishutz 21b68b1e68 [Hyprland] Fix alvr launch bind typo 2025-10-22 12:55:57 +02:00
janishutz fa4929a94b [Hyprland] Screenshot binds exit submap 2025-10-22 12:53:44 +02:00
janishutz 6b4d0e22b1 [Kitty] Remove default mapping interfering with new nvim bind 2025-10-21 08:44:42 +02:00
janishutz d5615892e7 [Hyprland] Add launch command for alvr and bsm 2025-10-17 17:30:04 +02:00
janishutz 81d0df6023 [Hyprland] Fix notifications submap 2025-10-17 10:58:39 +02:00
janishutz 9cf14d3b55 [Astal] Format 2025-10-17 10:47:05 +02:00
janishutz 06325b09b8 [Hyprland] Update readmes 2025-10-17 10:44:33 +02:00
janishutz 539ec34b4c [Astal] Finish modes, move tray into quickactions menu 2025-10-17 10:44:33 +02:00
janishutz 00d4f101fc [Hyprland] Remove some quick access binds (unnecessary) 2025-10-17 09:55:15 +02:00
janishutz c9a89cf545 [Setup] Fix platform wrong 2025-10-17 09:54:16 +02:00
janishutz 7ba9a65f19 [Build] Add linter 2025-10-17 09:47:40 +02:00
janishutz 52ced69697 [Hyprland] Fix submaps 2025-10-17 09:47:25 +02:00
janishutz 47bbdd9f85 [Astal] Add mode indicator 2025-10-17 09:47:18 +02:00
janishutz 94a57be9f7 [Astal] Add colours 2025-10-17 07:29:18 +02:00
janishutz 9d779269eb [Hyprland] Notifications binds 2025-10-17 06:52:15 +02:00
janishutz da61dc869d [Astal] Add new mode for hyprland 2025-10-17 06:51:05 +02:00
janishutz e44cc65f1f [Astal] Start adding hyprland module 2025-10-16 22:25:48 +02:00
janishutz 0392f8fc0f [Hyprland] Launch submap autoreset 2025-10-16 22:25:35 +02:00
janishutz 3c76f0412a [Setup] Fix hyprmode apply 2025-10-16 17:05:11 +02:00
janishutz 627c005fe3 [Hyprland] Fix submap 2025-10-16 17:05:03 +02:00
janishutz 6540bda07a [Hyprland] Add new bind layout 2025-10-16 16:18:08 +02:00
janishutz a9c73ba9d9 [Scripts] Improve VPN script 2025-10-16 10:44:27 +02:00
janishutz 30488edaed [Fish] Fix latexdocs alias 2025-10-10 15:05:28 +02:00
janishutz 8ed711d300 [Fish] Improve latex doc opening 2025-10-09 21:08:06 +02:00
janishutz a8043a9899 [Hyprland] Speed up animations even more 2025-10-09 11:02:51 +02:00
janishutz aa9ebaf22b [Astal] Battery widget: Show waiting 2025-10-09 09:33:21 +02:00
janishutz 2314a17066 [Install] Add ntp 2025-10-06 13:11:34 +02:00
janishutz 7051688fa5 [Clang] Formatter: Only merge empty functions 2025-10-05 12:13:56 +02:00
janishutz 0f4f601a2d [Clang] Improve formatter config 2025-10-05 12:09:53 +02:00
janishutz f38e2ff94f [Clang] Improve formatter config 2025-10-05 11:58:55 +02:00
janishutz 50c5cf945d [Clang] Improve formatter config 2025-10-05 08:38:26 +02:00
janishutz 903da245ac [Termfilechooser] Fix 2025-10-04 08:53:07 +02:00
janishutz 2d15296597 [Fish] Force pager into pager mode and not cat 2025-10-02 14:39:15 +02:00
janishutz 3a619cbf92 [Hyprland] Add new keybind on laptop 2025-10-02 14:21:32 +02:00
janishutz 530022ee59 [Hyprlock] Update clock 2025-10-02 08:39:03 +02:00
janishutz 5c01a5f806 [LatexIndent] Fix path 2025-10-01 15:34:14 +02:00
janishutz b6f5ad9bcf [Install] Add man 2025-10-01 13:44:15 +02:00
janishutz 92dc644ee2 [Fish] Add alias for gcc with error flags 2025-10-01 12:42:14 +02:00
janishutz ddc39f4928 [Setup] Fix indentconfig copying 2025-10-01 10:48:30 +02:00
janishutz a93d7494d3 [Install] Add sysstat package 2025-10-01 10:47:13 +02:00
janishutz 706cff40b3 [Setup] Fix errors with startup 2025-09-30 14:03:26 +02:00
janishutz e35845099a [Hyprland] Update for new folder structure 2025-09-30 07:41:44 +02:00
janishutz c44ef61cf2 [Fish] Update for new folder structure 2025-09-30 07:41:14 +02:00
janishutz ef65f827a2 [Yazi] Update keymap for new structure 2025-09-29 11:48:29 +02:00
jh04 6b8f415703 [Eslint] Update rules 2025-09-28 17:08:48 +02:00
janishutz f7004d9f25 [All] Remove old files 2025-09-27 08:07:58 +02:00
jh04 58d5b6cf69 [Setup] Add reload to make sure config reload happens properly 2025-09-26 14:04:47 +02:00
jh04 c55f70b852 [Fish] Add alias for latex docs 2025-09-26 14:03:21 +02:00
janishutz 97e02d81af [Clang] Fix config file 2025-09-25 16:42:26 +02:00
janishutz a545bb0066 [Clang] Fix incorrect config 2025-09-25 16:40:53 +02:00
janishutz 35d51f80df [Clang] Finish up config 2025-09-25 15:57:39 +02:00
janishutz d638aa0434 [Clang] Fix setup script 2025-09-25 15:52:55 +02:00
janishutz 258fc61669 [Clang] Add formatter config 2025-09-25 15:52:16 +02:00
janishutz 6dc15047d4 [Eslint] Add sort-imports rule 2025-09-25 15:20:13 +02:00
janishutz 0725763b95 [Hyprland] Configure gestures 2025-09-25 15:04:47 +02:00
jh04 29b8f188e9 [Yazi] Update keymap 2025-09-25 11:04:07 +02:00
jh04 959732ffd1 [Yazi] Update keymap 2025-09-23 15:19:36 +02:00
janishutz be3497de77 [Indent] Add more rules 2025-09-23 11:14:26 +02:00
janishutz 17d83a53b7 [Hyprland] configure gestures 2025-09-23 11:12:53 +02:00
janishutz 122f3ab070 [Hyprland] Add special workspace config 2025-09-22 16:17:22 +02:00
janishutz 46a732e8f7 [Install] Add dep for latexindent 2025-09-22 13:44:53 +02:00
janishutz 3373ae3b8c [Install] Add dep for latexindent 2025-09-22 13:37:50 +02:00
janishutz edd2db313c [Install] Add dep for latexindent 2025-09-22 13:36:37 +02:00
janishutz ecfd517a4a [Hyprland] Tweak animation duration 2025-09-22 13:01:13 +02:00
janishutz 25419067d4 [Install] Add note on using nwg-look 2025-09-22 12:53:46 +02:00
janishutz 36faff6b5f [Install] Add langgerman to install 2025-09-22 08:44:27 +02:00
janishutz ffe516175c [Eslint] Revert EOL decision 2025-09-20 08:12:20 +02:00
janishutz f58f22cadd [Install] Add german latex extras 2025-09-20 07:17:56 +02:00
janishutz 7e9faff6d6 [Astal] Make brightness adjust only in 5% intervals 2025-09-18 07:52:41 +02:00
janishutz ffa25045e5 [zathura] Add poppler to install 2025-09-17 16:17:43 +02:00
jh04 66dcd82952 [Hyprland] Fix bind 2025-09-16 17:31:28 +02:00
jh04 40013a61d1 [Hyprland] Fix some binds 2025-09-16 17:30:37 +02:00
janishutz ee26c0c5c2 [Hyprland] Fix errors 2025-09-16 13:03:25 +02:00
janishutz 02a351768f [Fish] Add pager alias 2025-09-16 12:59:20 +02:00
janishutz 9539f09e83 [Install] Add pager 2025-09-16 12:59:20 +02:00
jh04 25a78f126e [Astal] Add battery low notification 2025-09-15 14:42:05 +02:00
jh04 e0c339785d [Eslint] Update eol-last 2025-09-15 10:32:59 +02:00
jh04 e6f57831b1 [Eslint] Enforce newline 2025-09-15 10:27:26 +02:00
jh04 4e323d932f [Hyprland] Update binds 2025-09-15 09:20:29 +02:00
janishutz 64d8ca7ab4 [Install] Add eslint_d 2025-09-14 07:29:12 +02:00
jh04 7de17aaeb7 [Yazi] Update keymaps for ETH folders 2025-09-13 17:24:37 +02:00
janishutz 4f0c6a45cf [Install] Add http-server 2025-09-13 08:29:42 +02:00
janishutz a2dbdd78d3 [Setup] Don't do autologin (will make optional) 2025-09-13 08:22:11 +02:00
jh04 e4a76c494c [Greetd] Add auto-unlock of keyring 2025-09-12 17:26:53 +02:00
janishutz 1ee3f01883 [Setup] Add note 2025-09-12 13:12:13 +02:00
janishutz a89ed00a17 [Greetd] Add autologin 2025-09-12 12:26:47 +02:00
janishutz 64e671119d [Hyprland] Add dimming around rofi layer 2025-09-12 11:54:55 +02:00
janishutz 7b7955ac29 [Build] Run config build 2025-09-12 11:52:00 +02:00
jh04 823a8d8fbc [Setup] Make it quicker for small changes 2025-09-12 11:49:34 +02:00
jh04 088562cd53 [System] Remove mkinitcpio config 2025-09-12 11:49:26 +02:00
janishutz e9b8c35b52 [Scripts] Rename 2025-09-12 11:42:53 +02:00
janishutz 5a1811979e [Rofi] Fix positioning 2025-09-12 11:41:21 +02:00
janishutz 274890281c [Install] Add ripgrep 2025-09-12 10:40:46 +02:00
janishutz 0606ec06f5 [Hyprland] Add new rules 2025-09-12 10:40:11 +02:00
jh04 f22f8da68c [Install] Add git-credential-manager 2025-09-12 10:19:00 +02:00
jh04 409078edf9 [Install] Add ruby 2025-09-12 10:15:18 +02:00
jh04 2840c9aa70 [Fish] Add fish_variables 2025-09-12 10:13:22 +02:00
jh04 f4aedf7b5e [Install] Fix packages 2025-09-12 10:10:58 +02:00
jh04 f51cc0049a [Fish] Disable greeting 2025-09-12 10:10:48 +02:00
jh04 c01529a599 [Fish] Remove autojump from config 2025-09-12 09:57:40 +02:00
jh04 121f02c785 [Yazi] Fix keymap overlap 2025-09-12 09:57:33 +02:00
jh04 f6cd98e458 [Install] Add more utilities 2025-09-12 09:57:25 +02:00
jh04 cc9f38c71c [Install] Add more features 2025-09-12 09:29:18 +02:00
jh04 606a1fec1b [Install] Add auto-install of arch4edu and yay 2025-09-12 08:46:19 +02:00
janishutz d3bf33a142 [Install] Add seahorse to install 2025-09-12 06:59:29 +02:00
janishutz 91a6566d68 [Install] Finish up 2025-09-11 20:25:42 +02:00
jh04 ce0d951a54 [Install] Reorganize 2025-09-11 09:22:29 +02:00
jh04 106fe9cd9e Restructure bar restart and fix typos in ethz-vpn 2025-09-09 13:28:31 +02:00
jh04 a199569aac [Eslint] Increase max line length 2025-08-28 15:44:56 +02:00
jh04 80d16ab6ff [Install] Properly install yazi plugins 2025-08-22 14:21:19 +02:00
janishutz 1c8b72867e [Yazi] Add new plugin 2025-08-22 12:00:06 +02:00
jh04 9d59b24995 [Scripts] Add script to update open-webui 2025-08-21 12:14:30 +02:00
jh04 e78d07b25b [Base] Remove unnecessary files 2025-08-20 17:25:48 +02:00
jh04 a2c11e5cd1 [Yazi] Add keybind to open in evince 2025-08-05 11:21:27 +02:00
janishutz 70aceee3ca [Build] Build config 2025-07-31 09:08:07 +02:00
janishutz 8e6340e4c7 [Hyprlock] Update to V0.9 2025-07-31 09:07:19 +02:00
janishutz 46a4d7628d [Yazi] Add keybinds, start configuring openers 2025-07-28 11:10:03 +02:00
janishutz 2ec9356aca [Scripts] Prepare for cleanup 2025-07-22 15:52:52 +02:00
janishutz 3c19159c23 [Install] Use bvi instead of ghex 2025-07-16 15:38:45 +02:00
jh04 f6afd785fd [Setup] Fix yazi plugin updates 2025-06-22 15:21:45 +02:00
jh04 e20a33e132 [Setup] Fix 2025-06-22 14:49:58 +02:00
jh04 4683a171b9 [Astal] Finish up audio fix 2025-06-18 11:59:03 +02:00
jh04 4189258def [Astal] Fix audio issues 2025-06-18 11:40:58 +02:00
janishutz 2e8f1ec9ae [Hypr] Add easy docked switching 2025-06-12 10:08:57 +02:00
janishutz 78c97c5d84 [Hypr] Add docked config 2025-06-12 10:05:16 +02:00
jh04 4540dc00e7 [Eslint] Fix newline 2025-06-11 10:56:47 +02:00
jh04 294d7a9a98 [Eslint] Add max-len config 2025-06-11 09:48:34 +02:00
jh04 77dd4b4ca8 [Eslint] Add function-paren-newline option 2025-06-11 09:43:48 +02:00
jh04 7394ab63bf [Eslint] Change a few options 2025-06-10 17:23:43 +02:00
jh04 7f851fac73 [Eslint] Fix errors with single-line interface defintions 2025-06-07 11:18:58 +02:00
jh04 a5a6d9a916 [Eslint] Fix issues with end of file extra line 2025-06-06 15:36:12 +02:00
jh04 3967a4cd5c [latexfmt] Update align delims 2025-06-06 13:27:45 +02:00
jh04 f684f52d8f [latexfmt] Add indents for code 2025-06-06 13:20:52 +02:00
jh04 1079f62c06 [Eslint] Fix no-extra-parens config 2025-06-06 13:20:39 +02:00
jh04 1f460ccc38 [Eslint] Add support for mjs and cjs, format config 2025-06-06 11:46:57 +02:00
jh04 8b7f15eddf [Eslint] Add padding guides 2025-06-06 11:36:04 +02:00
jh04 0750c45111 [latexfmt] Finish up latexindent config 2025-06-06 10:59:16 +02:00
jh04 0db1815665 [Eslint] Fix spelling error in config 2025-06-06 10:35:04 +02:00
jh04 d028b027c5 [Eslint] Add more style options 2025-06-06 10:34:01 +02:00
jh04 e91f5aa5d6 [Eslint] Fix end of file newline 2025-06-06 10:27:49 +02:00
jh04 6105341234 [Eslint] Finish up config 2025-06-06 09:33:57 +02:00
jh04 6a362a963f [Eslint] Fix config 2025-06-05 19:59:45 +02:00
jh04 9005bd089a [Hyprland] Increase border size 2025-06-05 17:53:49 +02:00
jh04 1921545d08 [Hyprland] Fix keybind for cliphist 2025-06-05 15:40:00 +02:00
jh04 05b463699f [Install] Add cliphist 2025-06-05 15:38:19 +02:00
jh04 6f33358f4a [Hyprland] Add cliphist 2025-06-05 15:37:44 +02:00
jh04 909983d069 [Scripts] Fix indentconfig.yaml 2025-06-05 14:10:04 +02:00
jh04 a259206de8 [Scripts] add indentconfig.yaml 2025-06-05 13:33:52 +02:00
jh04 3f058cbfd3 [Scripts] Add a script to automatically rename files
This script automatically fixes any file name errors to ensure all files
are named correctly. Use with caution, as I have not yet tested it much
2025-06-04 14:26:32 +02:00
janishutz 318fc39b61 [Astal] Fix syntax 2025-05-26 15:05:07 +02:00
janishutz 6b1c8dec58 [Setup] Fix syntax error 2025-05-22 16:47:46 +02:00
janishutz 3541ec68e7 [Hyprland] Update binds (remove unnecessary ones) 2025-05-22 16:47:33 +02:00
janishutz 3a3674670b [Hyprland] Update binds 2025-05-22 16:43:27 +02:00
janishutz b2c6d72a2f [Hypridle] Improve idle manager 2025-05-22 11:46:20 +02:00
janishutz b9c68535d4 [Hypridle] Remove 100s timout 2025-05-22 11:45:28 +02:00
janishutz ae5226202a [Install] Remove TODO 2025-05-22 11:27:07 +02:00
janishutz 580aee4ad5 [Vimiv] Add notes and alias for fish 2025-05-22 10:12:12 +02:00
jh04 a8bf678d45 [Astal] Increase max-width of hyprland active window 2025-05-20 17:04:28 +02:00
janishutz 73feae1142 [Hyprland] Change to dwindle layout 2025-05-20 15:19:18 +02:00
janishutz b26b870bb2 [Hyprland] Resize window keybinds 2025-05-20 14:35:39 +02:00
janishutz 2d3a6ad33f [Astal] Remove debug code 2025-05-20 14:24:25 +02:00
janishutz ef9439b92c [Hyprland] Improve gestures 2025-05-19 14:29:40 +02:00
jh04 c635f3bfa9 [Build] Run build 2025-05-19 10:02:44 +02:00
jh04 7c3eb71150 [Astal] Update memory icon 2025-05-19 09:47:12 +02:00
janishutz 4fef18041f [Notes] Add another todo 2025-05-16 07:39:25 +02:00
janishutz df3384c667 [Notes] Mark completed items, add new todo 2025-05-16 07:37:49 +02:00
janishutz 7a97d7425e [Bar] Ellipsize too large text 2025-05-15 17:07:06 +02:00
jh04 73ca687358 [Fish] Add more aliases 2025-05-13 17:26:44 +02:00
jh04 88441958d9 [Yazi] Add more keybinds 2025-05-13 17:26:36 +02:00
janishutz e59af388b1 [Kitty] Finish improved cursor_trail settings 2025-05-12 14:17:24 +02:00
janishutz 39da9176ad [Build] Run build 2025-05-12 13:49:46 +02:00
janishutz e4ee9c9dc8 [Kitty] Improve cursor trail 2025-05-12 13:48:54 +02:00
jh04 feef8452f4 [Install] Add new programs, restructure 2025-05-09 11:12:02 +02:00
janishutz c3ff5a58d6 [AGS] Improve Battery display 2025-05-08 12:53:53 +02:00
janishutz a005189aef [Setup] Make bars restart, will be writing new notifications stuff soon 2025-05-08 10:55:31 +02:00
janishutz e98f5c2bd7 [Build] Run build 2025-05-08 10:43:00 +02:00
janishutz e7f6be4291 [AGS] QuickView: Use better battery icon 2025-05-08 10:42:55 +02:00
janishutz 4fa7dc3c1a [Eslint] Update arrow-parens to as-needed 2025-05-05 16:15:19 +02:00
janishutz 2ccb49f24b [Eslint] Finish config 2025-05-05 15:50:56 +02:00
jh04 ec04ebee66 [Player] Fix time display 2025-05-05 12:01:07 +02:00
jh04 2b2f1c2b66 [Build] Run build 2025-05-03 16:57:46 +02:00
jh04 21f7e53b2b [Build] Fix changeable colours 2025-05-03 16:57:39 +02:00
janishutz 5f09b7b915 [Install] Add new program 2025-04-29 15:35:19 +02:00
janishutz 688013a6b8 [Hyprland] Add logging for bar & notifier to temp folder 2025-04-29 14:47:56 +02:00
jh04 4e7c3ceee8 [Yazi] Remove plugins from here 2025-04-28 10:16:30 +02:00
jh04 390874374e [Hyprland] Hyprlock: Animations 2025-04-28 10:16:30 +02:00
janishutz 7f7a755e32 [Hyprland] Fix merge error 2025-04-28 08:56:38 +02:00
jh04 ac6b47449a [README] Update 2025-04-27 17:11:41 +02:00
jh04 ce5f530ed2 Merge V2 complete 2025-04-27 17:07:20 +02:00
jh04 62789224a7 Merge 2025-04-27 16:56:43 +02:00
janishutz 30a2b2a0ad [AGS] Sysinfo: Launch terminal asynchronously 2025-04-27 10:50:05 +02:00
janishutz 710eeb7fb4 [AGS] Brightness: Fix, add CLI 2025-04-27 10:48:07 +02:00
janishutz 364b5cb0ef [AGS] Bar: Add tooltips 2025-04-27 10:10:16 +02:00
janishutz d9777df0fb [Lint] Add linter config for eslint, not done yet 2025-04-27 10:01:54 +02:00
jh04 dabb325436 [AGS] Fix Bluetooth menu 2025-04-26 21:14:05 +02:00
jh04 08347196f2 [Wlogout] Design fix 2025-04-26 21:00:25 +02:00
jh04 3553d03b25 [Rofi] Design update 2025-04-26 21:00:15 +02:00
jh04 f4b58b08ba [AGS] Fixes, BT apparently crashing 2025-04-26 21:00:01 +02:00
janishutz 1a4610a2d6 [AGS] Setup script improvements 2025-04-26 16:05:07 +02:00
janishutz 8c4af4f817 [Build] Run build, remove useless colours 2025-04-26 15:54:35 +02:00
jh04 4aa4bc57b9 [Wlogout] Improve looks 2025-04-26 15:35:44 +02:00
jh04 11c961bfe7 [AGS] SysInfo: Re-Structure for better perf 2025-04-26 15:32:47 +02:00
jh04 090492ae51 [Build] Give option to customize colours 2025-04-26 15:10:26 +02:00
jh04 b4b8d04e6a [AGS] SysInfo: Improve performance 2025-04-26 15:07:49 +02:00
jh04 2fd32da1b2 [AGS] Battery: Design complete 2025-04-26 13:30:22 +02:00
jh04 86e963e408 [AGS] Battery, Brightness: Improve design 2025-04-26 13:29:16 +02:00
jh04 678afa99d6 [AGS] Battery: Try to improve design 2025-04-26 13:26:52 +02:00
jh04 acca70d47f [AGS] Battery: Improve design 2025-04-26 13:24:34 +02:00
jh04 bec7474793 [AGS] Networking: Fix button display 2025-04-26 13:22:32 +02:00
jh04 414d42072a [AGS] Brightness improvements 2025-04-26 13:20:37 +02:00
jh04 1239f49cc7 [AGS] Brightness control 2025-04-26 13:18:00 +02:00
jh04 a7e6584f78 [AGS] Improve brightness controls 2025-04-26 13:07:55 +02:00
jh04 d72c4fdf86 [AGS] Potential fix for network crash 2025-04-26 13:03:46 +02:00
janishutz a8e0c903c4 [Build] Do full build 2025-04-26 12:20:32 +02:00
jh04 69d9b510f1 [AGS] Remove network speed info 2025-04-26 12:18:42 +02:00
jh04 f66c8639d2 [Greetd] Add configs 2025-04-26 12:14:57 +02:00
jh04 519eea601c [Build] Run build 2025-04-26 12:10:09 +02:00
jh04 609f662eb5 [Notes] Tick off completed items 2025-04-26 12:10:04 +02:00
jh04 651030fd75 [Setup] Improve script 2025-04-26 12:09:55 +02:00
jh04 212296a7d5 [AGS] Add new colour option 2025-04-26 12:09:38 +02:00
jh04 f85c5f1b10 [Rofi] Add renderables 2025-04-26 12:09:30 +02:00
jh04 590ccb5c9e [Rofi] Add configs 2025-04-26 12:09:07 +02:00
jh04 c7064dbea0 [Bar] Small bugfix for workspace number 2025-04-26 12:08:58 +02:00
jh04 afe25a322a [AGS] Launcher: Prepare 2025-04-26 12:08:33 +02:00
jh04 02861efcae [AGS] Remove old modules 2025-04-26 12:08:22 +02:00
jh04 6c1c9ad8cf [Build] Restructure 2025-04-26 12:08:16 +02:00
jh04 f2bdddb9b6 [AGS] Add new notifications setup 2025-04-26 09:49:03 +02:00
jh04 e19a1179d5 [AGS] Fix Bluetooth crashes 2025-04-26 09:38:21 +02:00
janishutz 33fcdd72f7 [Notes] Tick off what is completed 2025-04-26 08:55:22 +02:00
jh04 bf7d61945b [AGS] Possibly fixed 2025-04-25 21:36:19 +02:00
jh04 03d9d89102 [AGS] Fix crashing 2025-04-25 20:56:59 +02:00
jh04 3e5136fdbb [Meson] Prepare 2025-04-25 18:14:42 +02:00
jh04 99a7a59cf7 [Build] Rebuild 2025-04-25 17:22:15 +02:00
jh04 54e216b5ec [AGS] Fixes for bar 2025-04-25 17:22:07 +02:00
jh04 19d59347b6 [AGS] Cleanup 2025-04-25 16:58:19 +02:00
jh04 9f754b454b [Build] Fix up 2025-04-25 16:33:48 +02:00
jh04 7f21fb3ee3 [Build] Do full build 2025-04-25 16:16:14 +02:00
jh04 0b349fb038 [Build] Done 2025-04-25 15:36:22 +02:00
jh04 db16e830d1 [Gtk-Theme] Done 2025-04-25 15:36:18 +02:00
jh04 d120b7a49e [Gtk-Theme] Pre-Processing + Some other work 2025-04-25 10:05:42 +02:00
jh04 0b849bad4f Fish change alias bt 2025-04-25 08:55:02 +02:00
jh04 722be458ec [Gtk-Theme] First setup 2025-04-25 08:52:52 +02:00
jh04 aba9e0f30a [Build] Prepare for improved rendering capbilities 2025-04-25 07:43:53 +02:00
janishutz 49446646f3 [Notes] Tick off what is complete 2025-04-25 06:58:26 +02:00
janishutz b5ad7e3034 [Notes] Some changes 2025-04-25 06:53:17 +02:00
jh04 10136ab9de [AGS] Bar: Done (WiFi still missing, will be added at some later point) 2025-04-24 16:58:30 +02:00
jh04 e93e051094 [AGS] Bar: BT, Audio, SysInfo, Brightness 2025-04-23 20:04:48 +02:00
jh04 69484fc302 [AGS] Bar: Improve QuickActions 2025-04-22 17:38:41 +02:00
jh04 8a2180e120 [AGS] GTK 4 Migration: Done, Start adding QuickActions
Probably gonna abandon the QuickActions, as that is just way too much
effort for what it does. Will be providing keybinds for doing what I
wanted to do there in Hyprland
2025-04-22 15:30:41 +02:00
jh04 8b70f80e60 [AGS] GTK4 Migration: Partially complete 2025-04-21 21:48:29 +02:00
jh04 0022880936 Actually create kitty config 2025-04-21 20:26:45 +02:00
jh04 7380c75818 [AGS] Save for migration to GTK4 2025-04-21 17:26:52 +02:00
jh04 f4b259dd13 [AGS] Bar: Start sysinfo 2025-04-21 12:44:41 +02:00
jh04 b2f1d8fd9e [AGS] Bar progress 2025-04-19 18:20:32 +02:00
jh04 78e472beb8 [AGS] Redo basic setup 2025-04-19 15:20:50 +02:00
jh04 a9c7b7d7ee [AGS] Prepare bar 2025-04-19 15:08:42 +02:00
janishutz 1d5240b7c6 Add terminal file chooser 2025-04-14 16:51:42 +02:00
janishutz 196d553627 [Icons] Add gen list 2025-04-14 16:49:24 +02:00
janishutz ed17459e2a [Launcher] Improve type definitions 2025-04-14 16:17:21 +02:00
janishutz 0b0e83f505 Improve notifications 2025-04-14 15:23:28 +02:00
janishutz d9fdf1ee6d [Launcher] Add helper for fzf 2025-04-10 15:01:02 +02:00
janishutz f4cfa20dea Add fastfetch config 2025-04-10 11:35:39 +02:00
jh04 4dc14cf4e3 [AGS] Setup 2025-04-09 19:25:06 +02:00
jh04 d866102fcc Improve copying in Zathura 2025-04-09 19:20:00 +02:00
jh04 921373f049 Update package list 2025-04-09 10:13:54 +02:00
jh04 1c39478650 Remove VSCode forced workspace 2025-04-05 15:19:55 +02:00
janishutz c70a3ffdb4 [Launcher] Prepare component 2025-04-04 07:43:08 +02:00
jh04 a845e086fd Change switch workspace keybind 2025-04-03 11:06:32 +02:00
jh04 b363438f6a Add zathura launch bind 2025-04-03 08:35:54 +02:00
jh04 3bb9c6e661 [Install] Add notes 2025-04-02 11:14:12 +02:00
jh04 7d9da82b6c Add restore plugin, improve ouch for yazi 2025-04-02 11:14:00 +02:00
jh04 c52c5b5a42 Improve Zathura, move to zoxide, add yazi plugins 2025-04-02 11:08:15 +02:00
jh04 97c49f17e6 Add nohlsearch to keybind zathura 2025-04-02 10:40:04 +02:00
jh04 134519558a Remove some zathura binds that come by default 2025-04-02 10:31:09 +02:00
jh04 9e5ae7db83 Update zathura binds 2025-04-01 17:52:33 +02:00
jh04 a31b3481c3 Make zathura open on 2nd workspace 2025-04-01 17:44:07 +02:00
jh04 1c757d1173 Add zathura config 2025-04-01 17:40:25 +02:00
jh04 89b97c4ba1 Add Zathura config 2025-04-01 17:40:10 +02:00
jh04 2ba627a370 Hyprland revert mouse focus change 2025-04-01 15:57:44 +02:00
jh04 995de3617c Hyprland change mouse focus 2025-04-01 13:50:28 +02:00
janishutz dda261ac3c [Notifications] Only enable menu if there are notifications 2025-03-31 17:52:12 +02:00
janishutz a477078f16 Hyprland fix vivado window rules 2025-03-31 15:33:15 +02:00
janishutz f93eeb5d87 Hyprland improve vivado 2025-03-31 15:17:32 +02:00
jh04 5b0e3767bd Add yazi keybinds 2025-03-31 10:50:44 +02:00
jh04 791f10aad4 [Launcher] Prepare UI 2025-03-30 10:56:18 +02:00
janishutz 51962c424b Hyprland fix for V0.48 2025-03-30 08:09:44 +02:00
janishutz 666a047814 [Launcher] fzf, cmd processing, args 2025-03-30 08:00:10 +02:00
jh04 c0e9062230 [Yazi] Add additional themes 2025-03-29 13:37:42 +01:00
jh04 c9442acce8 [Build] Improve treeWalker, renderer & wallpaper finder 2025-03-29 10:30:08 +01:00
jh04 a6ce7ccdce Fix path for scripts 2025-03-28 13:50:42 +01:00
janishutz 0f0e0e8671 improve notifications keybinds 2025-03-28 11:45:42 +01:00
janishutz 527f2012de [Notifications] Finish up toggle 2025-03-28 07:37:14 +01:00
janishutz c6061e0f12 Remove quit keybind from hyprland. Can be done through wlogout 2025-03-27 16:23:12 +01:00
jh04 c2f38bc39a [Build] Improve rendering, add fs parsing
FS parsing needs a lot of improvements (namely ignoring gitignore
files). Probably gonna migrate to a library for this
2025-03-25 20:06:50 +01:00
jh04 bab328c2d3 [Build] Tons of improvements 2025-03-25 19:51:37 +01:00
janishutz c53069f0df [Bar] Prepare 2025-03-25 17:13:03 +01:00
janishutz 7c15b0b320 [Launcher] Setup 2025-03-25 17:12:57 +01:00
janishutz 3d74676f6d [QuickActions] Prep 2025-03-25 17:12:48 +01:00
janishutz 994d1ac1b4 [Hypr] Hyprlock template 2025-03-25 17:12:36 +01:00
janishutz aea9229c54 [Build] Prep 2025-03-25 17:12:09 +01:00
janishutz 49595c3811 Restructure, move launcher to astal gtk4 2025-03-25 13:51:56 +01:00
janishutz 4a769d9eb0 Add more notes 2025-03-24 15:36:51 +01:00
jh04 cac0e41a2e Change shortcuts for fish 2025-03-24 09:12:07 +01:00
jh04 840691ec67 Improve launcher 2025-03-23 14:11:02 +01:00
jh04 d387d02534 [Notifications] Add toggle 2025-03-23 14:10:54 +01:00
jh04 c4797dd592 Remove killactive from MainMod + C 2025-03-22 20:41:22 +01:00
jh04 7ff986e7a6 [Notifications] Improve handling of centre 2025-03-22 14:28:31 +01:00
jh04 8986ad894e Improve keybindings 2025-03-22 11:31:46 +01:00
jh04 709af40296 Restructure, prepare launcher 2025-03-22 11:26:57 +01:00
jh04 3060c2b06e Restructure 2025-03-22 11:26:26 +01:00
jh04 f127ea8a41 Fix binds for new notification handler 2025-03-21 18:25:36 +01:00
jh04 1f9feeb85f Prepare QuickActions 2025-03-21 18:24:36 +01:00
jh04 9d0574ecab Remove old preparation 2025-03-21 18:24:27 +01:00
jh04 e483d7de23 Finish basic notification setup 2025-03-21 18:23:44 +01:00
jh04 7fe86701b3 Prepare Launcher 2025-03-21 18:23:35 +01:00
jh04 ca3d9c32e9 Prepare Bar 2025-03-21 18:23:29 +01:00
jh04 bb75cfabaf Add binds for new notification handler 2025-03-21 18:10:15 +01:00
jh04 c5b1f64376 Add ags stuff, but will probably redo entirely 2025-03-21 16:16:56 +01:00
janishutz 847ecc8ef7 Prepare for new configs 2025-03-20 18:57:39 +01:00
janishutz e79534ff45 Add new installed packages list 2025-03-20 13:20:54 +01:00
janishutz 906498cf67 Clean up rofi config 2025-03-20 13:20:46 +01:00
janishutz 62c7a61dd2 Add ETHZ-VPN script by Glib 2025-03-20 13:12:22 +01:00
janishutz 5ff87ba160 Add new shortcuts for yazi 2025-03-20 12:55:07 +01:00
jh04 f2958885db Update configs from PC 2025-03-20 08:38:40 +01:00
jh04 11a765543f fix setup script 2025-03-18 17:44:24 +01:00
janishutz b2ebac4094 [yarzi] Add keybind to go to trash 2025-03-18 16:10:30 +01:00
janishutz a3693a8ad1 Improve config collection, add rofi config for pc 2025-03-18 14:19:19 +01:00
janishutz 87298afcb5 Update rofi collection script 2025-03-18 14:12:15 +01:00
janishutz e894d77ebb Waybar: Add keyboard-state for caps 2025-03-18 13:42:39 +01:00
janishutz c876804917 Swap ESC and Caps through hyprland 2025-03-18 10:39:49 +01:00
janishutz 86aa16c028 Improve kmonad, not ready to be used 2025-03-18 10:24:43 +01:00
janishutz 7ca01632d3 Add initial kmonad setup 2025-03-17 17:21:20 +01:00
janishutz cdf2efa1a4 Add refocus rule 2025-03-17 16:51:50 +01:00
janishutz 5373190dda Improve yazi keymap 2025-03-17 14:16:16 +01:00
jh04 a9d92efa18 Improve install scripts, add more shortcuts, improve hypridle 2025-03-12 16:08:58 +01:00
janishutz b54a382841 Improve yazi config 2025-03-11 09:47:39 +01:00
janishutz 7a578cfb00 Add lazygit 2025-03-06 11:40:16 +01:00
janishutz 4fa6cb1616 Improve laptop config, add aliases 2025-03-04 14:49:52 +01:00
jh04 7cf3693887 Push new package list 2025-03-03 10:25:07 +01:00
jh04 c1afb50799 Upload from GitHub 2025-02-28 11:41:18 +01:00
350 changed files with 37621 additions and 2541 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
+29 -17
View File
@@ -1,35 +1,47 @@
<div id="title" align="center">
<img src="https://static.janishutz.com/logo.jpg" width="300">
<h1>janishutz dotfiles</h1>
<h1>janishutz Hyprland</h1>
</div>
Full system configuration using [Decman](https://github.com/kiviktnm/decman).
Collection of dotfiles for my personal Hyprland setup, running on Arch Linux.
Includes a setup and install script (that one is not complete yet though).
For my neovim config, see [here](https://git.janishutz.com/janishutz/nvim)
*Screenshot outdated, a new screenshot will be added when this update is completed*
![screenshot of desktop with 2 screens](./assets/screenshot.png)
# New Version
This version of the dotfiles (and all branches apart from `new`) are deprecated and in maintenance mode only,
while a new version is developed on the [new branch](https://git.janishutz.com/janishutz/dotfiles/src/branch/new)
## Features
- DankMaterialShell
- Automatically generated GTK Theme and theming of the rest of the desktop using `matugen`
- Astal4 based Status Bar and Quick Actions menu
- System info
- Hyprland info
- Date & Time
- Bluetooth picker
- some networking settings (more coming later)
- Audio and brightness control
- battery monitoring
- Logout, Reboot, Shutdown, etc
- Rofi config for App launcher
- Wlogout config
- Theming script that generates a GTK theme and theming for bar, Hyprland, etc
- Fish config (with some handy aliases, based on one from ohh-my-fish)
- Fastfetch config
- kitty config with cursor trail
- notification manager uses `swaync`
- Linter configs (eslint, clang-format, latexindent and more)
- Linter configs (currently only eslint, which is not complete yet)
- mpv config
- zathura configs
- yazi configs with links to various directories I use commonly plus a few plugins and themes that are applied by the script
- Custom QuickShell-based status bar and QuickActions menu (Postponed until further notice)
- Astal3 based Notifications (due to be migrated to Astal4)
## Installing
Clone your repo to any folder and adapt what you need. Some folders are still hard-coded, which I will be changing later on, so you will likely run into issues.
You may then run the `setup` script. That won't install all dependencies though. The `install` script is what serves that purpose, but that is not complete yet
## TODOs
- [ ] Migrate status bar to QuickShell
- [ ] Rewrite Hyprland configs using lua
- [ ] Waylogout still needed?
- [ ] Add the formatter configs back in
- [ ] Add the scripts back in
- [ ] Finish decman configuration to copy all the files
- [ ] Custom yazi theme
- [ ] Notifier
## Setting up to develop
Clone this repo locally. For `config/astal` and `config/ags`, you will want to run `ags types -d .` in every directory where there is a `app.ts` file as well as `mkdir node_modules && cd node_modules && ln -sf /usr/share/astal/gjs/ ./astal` to prepare for development.
The `config/ags` directory contains gtk3 config for astal, whereas in `config/astal`, gtk4 configs can be found. All modules but for the notifications are written in Gtk 4.
-16
View File
@@ -1,16 +0,0 @@
# TODOs
- [X] Notifications management via dms
- [X] Launcher via dms
- [X] DMS screenshots
- [X] Hyprland theming using mutagen with dms integration
- [ ] DMS config sync to here using script
- [ ] Remove `decman` because it's a pain and use smaller scope `archmgr` (only copy all files to locations always and package diff)
- [ ] Generate matugen stuff from colour in new script
- [X] Binds for other DMS features (with subcommand)
- [X] Finish decman config
- [X] Remove wlogout and replace with dms setup
- [ ] Yazi theming
- [X] Kitty theming
- [ ] Fish theming
- [ ] Fastfetch theming
- [X] DMS instead of Hyprlock for locker?
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+58
View File
@@ -0,0 +1,58 @@
// Using commonjs instead of ejs, because more widely compatible
const inquirer = require( 'inquirer' );
const fs = require( 'fs' );
const path = require( 'path' );
const os = require( 'os' );
const render = require( './helpers/render' );
const { treeWalker } = require('./helpers/util');
// Prompt user to select a wallpaper (if no path is passed as argument)
const wallpapers = treeWalker( path.join( os.homedir(), '/NextCloud/Wallpapers' ), '*' );
// const wallpapers = fs.readdirSync( path.join( os.homedir(), '/NextCloud/Wallpapers' ) );
const wallpaperChoices = [];
wallpapers.forEach(element => {
const name = element.split( '/' );
wallpaperChoices.push( { 'name': name[ name.length - 1 ].split( '.' )[ 0 ], 'value': element } );
});
// Selection options
const chooseWallpaper = {
'type': 'list',
'name': 'wallpaper',
'message': 'Choose the wallpaper to be used',
'choices': wallpaperChoices,
};
const chooseLockpaper = {
'type': 'list',
'name': 'lockpaper',
'message': 'Choose the lockscreen wallpaper to be used',
'choices': wallpaperChoices,
};
const chooseTheme = {
'type': 'list',
'name': 'theme',
'message': 'Choose the general colourway to be used',
'choices': [
{ name: 'Nordic', value: 'nordic' },
{ name: 'Deep-Dark', value: 'deep-dark' },
{ name: 'Material-You', value: 'material' },
{ name: 'Light', value: 'light' },
{ name: 'Bright', value: 'bright' },
]
}
// TODO: Add argument parsing
const args = process.argv.slice( 2 );
inquirer.default.prompt( [
chooseWallpaper,
chooseLockpaper,
chooseTheme
] ).then( answers => {
render( answers.wallpaper, answers.lockpaper, answers.theme );
} ).catch( () => {
process.exit( 1 );
} );
+487
View File
@@ -0,0 +1,487 @@
const util = require( './util' );
const renderColourAsRGB = util.renderColourAsRGB;
const renderColourAsRGBA = util.renderColourAsRGBA;
const renderColourAsHex = util.renderColourAsHex;
// ───────────────────────────────────────────────────────────────────
// ╭───────────────────────────────────────────────╮
// │ Theme generator (returns theme as object) │
// ╰───────────────────────────────────────────────╯
// ───────────────────────────────────────────────────────────────────
module.exports.generateTheme = (
theme, wallpaper, lockpaper, palette
) => {
return {
'wallpaper-path': wallpaper,
'lockpaper-path': lockpaper,
// ┌ ┐
// │ Colours │
// └ ┘
// ── Foreground ───────────────────────────────────────────────────
'colour-foreground-hex': renderColourAsHex( colours.foreground[ theme ] ),
'colour-foreground-rgb': renderColourAsRGB( colours.foreground[ theme ] ),
'colour-foreground-rgba': renderColourAsRGBA( colours.foreground[ theme ], 0.5 ),
'colour-foreground-rgba-07': renderColourAsRGBA( colours.foreground[ theme ], 0.7 ),
'colour-foreground-rgba-06': renderColourAsRGBA( colours.foreground[ theme ], 0.6 ),
'colour-foreground-rgba-05': renderColourAsRGBA( colours.foreground[ theme ], 0.5 ),
'colour-foreground-rgba-03': renderColourAsRGBA( colours.foreground[ theme ], 0.3 ),
'colour-foreground-rgba-02': renderColourAsRGBA( colours.foreground[ theme ], 0.2 ),
'colour-foreground-rgba-01': renderColourAsRGBA( colours.foreground[ theme ], 0.1 ),
// ── Accent foreground colour ─────────────────────────────────────
'colour-foreground-accent-hex': renderColourAsHex( colours['foreground-accent'][ theme ] ),
'colour-foreground-accent-rgba-07': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.7 ),
'colour-foreground-accent-rgba-06': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.6 ),
'colour-foreground-accent-rgba-05': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.5 ),
'colour-foreground-accent-rgba-03': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.3 ),
'colour-foreground-accent-rgba-02': renderColourAsRGBA( colours['foreground-accent'][ theme ], 0.2 ),
// ── Accent colour ────────────────────────────────────────────────
'colour-accent-hex': renderColourAsHex( palette[ 0 ] ),
'colour-accent-rgb': renderColourAsRGB( palette[ 0 ] ),
'colour-accent-rgba': renderColourAsRGBA( palette[ 0 ], 0.3 ),
'colour-accent-rgba-07': renderColourAsRGBA( palette[ 0 ], 0.7 ),
'colour-accent-rgba-05': renderColourAsRGBA( palette[ 0 ], 0.5 ),
'colour-accent-rgba-03': renderColourAsRGBA( palette[ 0 ], 0.3 ),
'colour-accent-rgba-02': renderColourAsRGBA( palette[ 0 ], 0.2 ),
'colour-accent-rgba-015': renderColourAsRGBA( palette[ 0 ], 0.15 ),
'colour-accent-rgba-011': renderColourAsRGBA( palette[ 0 ], 0.11 ),
'colour-accent-rgba-007': renderColourAsRGBA( palette[ 0 ], 0.07 ),
'colour-accent-hyprland': util.renderColourAsRGBAHex( palette[ 0 ], 0.8 ),
// ───────────────────────────────────────────────────────────────────
'colour-accent-gradient-1-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 1, gradientMultipliers[ theme ]
) ),
'colour-accent-gradient-2-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 2, gradientMultipliers[ theme ]
) ),
'colour-accent-gradient-3-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 3, gradientMultipliers[ theme ]
) ),
'colour-accent-gradient-4-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 4, gradientMultipliers[ theme ]
) ),
'colour-accent-gradient-5-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 5, gradientMultipliers[ theme ]
) ),
'colour-accent-gradient-inverse-1-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 1, 1 / gradientMultipliers[ theme ]
) ),
'colour-accent-gradient-inverse-2-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 1, 1 / gradientMultipliers[ theme ]
) ),
'colour-accent-gradient-inverse-3-hex': renderColourAsHex( util.getGradientColour(
palette[ 0 ], 1, 1 / gradientMultipliers[ theme ]
) ),
// ── Secondary accent ─────────────────────────────────────────────
'colour-accent-2-hex': renderColourAsHex( palette[ 1 ] ),
'colour-accent-2-rgb': renderColourAsRGB( palette[ 1 ] ),
'colour-accent-2-rgba-07': renderColourAsRGBA( palette[ 1 ], 0.7 ),
'colour-accent-2-rgba-05': renderColourAsRGBA( palette[ 1 ], 0.5 ),
'colour-accent-2-rgba-03': renderColourAsRGBA( palette[ 1 ], 0.3 ),
'colour-accent-2-rgba-02': renderColourAsRGBA( palette[ 1 ], 0.2 ),
'colour-accent-2-rgba-015': renderColourAsRGBA( palette[ 1 ], 0.15 ),
'colour-accent-2-rgba-01': renderColourAsRGBA( palette[ 1 ], 0.1 ),
'colour-accent-2-hyprland': util.renderColourAsRGBAHex( palette[ 1 ], 0.8 ),
// ── Tertiary accent ──────────────────────────────────────────────
'colour-accent-3-hex': renderColourAsHex( palette[ 2 ] ),
'colour-accent-3-rgb': renderColourAsRGB( palette[ 2 ] ),
'colour-accent-3-hyprland': util.renderColourAsRGBAHex( palette[ 2 ], 0.8 ),
// ── Background ───────────────────────────────────────────────────
'colour-background-hex': renderColourAsHex( colours.background[ theme ] ),
'colour-background-rgb': renderColourAsRGB( colours.background[ theme ] ),
'colour-background-rgba': renderColourAsRGBA( colours.background[ theme ], 0.5 ),
'colour-background-rgba-07': renderColourAsRGBA( colours.background[ theme ], 0.7 ),
'colour-background-rgba-05': renderColourAsRGBA( colours.background[ theme ], 0.5 ),
'colour-background-rgba-03': renderColourAsRGBA( colours.background[ theme ], 0.3 ),
'colour-background-rgba-02': renderColourAsRGBA( colours.background[ theme ], 0.2 ),
'colour-background-rgba-015': renderColourAsRGBA( colours.background[ theme ], 0.15 ),
'colour-background-rgba-011': renderColourAsRGBA( colours.background[ theme ], 0.11 ),
// ── Background Alternative ───────────────────────────────────────
'colour-background-alternative-hex': renderColourAsHex( colours[ 'background-alternative' ][ theme ] ),
'colour-background-alternative-rgb': renderColourAsRGB( colours[ 'background-alternative' ][ theme ] ),
'colour-background-alternative-rgba-07': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.7 ),
'colour-background-alternative-rgba-06': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.6 ),
'colour-background-alternative-rgba-05': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.5 ),
'colour-background-alternative-rgba-04': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.4 ),
'colour-background-alternative-rgba-03': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.3 ),
'colour-background-alternative-rgba-02': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.2 ),
'colour-background-alternative-rgba-015': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.15 ),
'colour-background-alternative-rgba-01': renderColourAsRGBA( colours[ 'background-alternative' ][ theme ], 0.1 ),
// ── Background Tertiary ──────────────────────────────────────────
'colour-background-tertiary-hex': renderColourAsHex( colours[ 'background-tertiary' ][ theme ] ),
'colour-background-tertiary-rgb': renderColourAsRGB( colours[ 'background-tertiary' ][ theme ] ),
'colour-background-tertiary-rgba-05': renderColourAsRGBA( colours[ 'background-tertiary' ][ theme ], 0.5 ),
'colour-background-tertiary-rgba-02': renderColourAsRGBA( colours[ 'background-tertiary' ][ theme ], 0.2 ),
// ── Shadow ───────────────────────────────────────────────────────
'colour-shadow-hex': renderColourAsHex( colours.shadow[ theme ] ),
'colour-shadow-rgb': renderColourAsRGB( colours.shadow[ theme ] ),
'colour-shadow-hyprland': util.renderColourAsRGBHex( colours.shadow[ theme ] ),
'colour-shadow-rgba-07': renderColourAsRGBA( colours.shadow[ theme ], 0.7 ),
'colour-shadow-rgba-05': renderColourAsRGBA( colours.shadow[ theme ], 0.5 ),
'colour-shadow-rgba-03': renderColourAsRGBA( colours.shadow[ theme ], 0.3 ),
'colour-shadow-rgba-02': renderColourAsRGBA( colours.shadow[ theme ], 0.2 ),
'colour-shadow-rgba-015': renderColourAsRGBA( colours.shadow[ theme ], 0.15 ),
'colour-shadow-rgba-011': renderColourAsRGBA( colours.shadow[ theme ], 0.11 ),
// ── Inactive ─────────────────────────────────────────────────────
'colour-inactive-hex': renderColourAsHex( colours.inactive[ theme ] ),
'colour-inactive-rgb': renderColourAsRGB( colours.inactive[ theme ] ),
'colour-inactive-rgba-07': renderColourAsRGBA( colours.inactive[ theme ], 0.7 ),
'colour-inactive-rgba-05': renderColourAsRGBA( colours.inactive[ theme ], 0.5 ),
'colour-inactive-rgba-03': renderColourAsRGBA( colours.inactive[ theme ], 0.3 ),
'colour-inactive-rgba-02': renderColourAsRGBA( colours.inactive[ theme ], 0.2 ),
'colour-inactive-rgba-015': renderColourAsRGBA( colours.inactive[ theme ], 0.15 ),
// ───────────────────────────────────────────────────────────────────
'colour-inactive-background-hex': renderColourAsHex( colours[ 'inactive-background' ][ theme ] ),
'colour-inactive-background-rgb': renderColourAsRGB( colours[ 'inactive-background' ][ theme ] ),
'colour-inactive-background-hyprland': util.renderColourAsRGBHex( colours[ 'inactive-background' ][ theme ] ),
'colour-inactive-background-rgba-07': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.7 ),
'colour-inactive-background-rgba-05': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.5 ),
'colour-inactive-background-rgba-04': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.4 ),
'colour-inactive-background-rgba-03': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.3 ),
'colour-inactive-background-rgba-02': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.2 ),
'colour-inactive-background-rgba-015': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.15 ),
'colour-inactive-background-rgba-01': renderColourAsRGBA( colours[ 'inactive-background' ][ theme ], 0.1 ),
// ┌ ┐
// │ Fonts │
// └ ┘
'font-primary': fonts.primary[ theme ],
'font-accent': fonts.accent[ theme ],
'font-mono': fonts.mono[ theme ],
// ┌ ┐
// │ Icon Theme │
// └ ┘
'icon-theme': iconTheme[ theme ],
// ┌ ┐
// │ yazi theme │
// └ ┘
'yazi-theme': yaziThemes[ theme ],
// ┌ ┐
// │ Path to this repo on disk │
// └ ┘
'path-to-dotfiles': __dirname.slice( 0, __dirname.length - 5 ),
};
};
// ───────────────────────────────────────────────────────────────────
// ╭───────────────────────────────────────────────╮
// │ Theme definitions │
// ╰───────────────────────────────────────────────╯
// ───────────────────────────────────────────────────────────────────
const gradientMultipliers = {
'nordic': 0.9,
'deep-dark': 0.8,
'material': 0.85,
'light': 1.1,
'bright': 1.15,
'test': 0.75
};
const colours = {
'foreground': {
'nordic': [
200,
220,
255
],
'deep-dark': [
230,
230,
230
],
'material': [
255,
255,
255
], // TODO: Will be calculated by material theme generator
'light': [
40,
40,
40
],
'bright': [
0,
0,
0
],
'test': [
0,
0,
0
],
},
'foreground-accent': {
'nordic': [
255,
255,
255
],
'deep-dark': [
255,
255,
255
],
'material': [
200,
200,
200
], // TODO: Will be calculated by material theme generator
'light': [
0,
0,
0
],
'bright': [
50,
50,
50
],
'test': [
0,
0,
0
],
},
'background': {
'nordic': [
10,
10,
15
],
'deep-dark': [
20,
20,
20
],
'material': [
30,
30,
30
], // TODO: Will be calculated by material theme generator
'light': [
230,
230,
230
],
'bright': [
255,
255,
255
],
'test': [
255,
255,
255
],
},
'background-alternative': {
'nordic': [
20,
20,
25
],
'deep-dark': [
30,
30,
30
],
'material': [
40,
40,
40
], // TODO: Will be calculated by material theme generator
'light': [
210,
210,
210
],
'bright': [
230,
230,
230
],
'test': [
255,
255,
0
] // brown
},
'background-tertiary': {
'nordic': [
0,
0,
0
],
'deep-dark': [
45,
45,
45
],
'material': [
0,
0,
0
], // TODO: Will be calculated by material theme generator
'light': [
180,
180,
180
],
'bright': [
200,
200,
200
],
'test': [
255,
0,
255
] // purple
},
'shadow': {
'nordic': [
0,
0,
2
],
'deep-dark': [
40,
40,
40
],
'material': [
30,
30,
30
], // TODO: Will be calculated by material theme generator
'light': [
190,
190,
190
],
'bright': [
150,
150,
150
],
'test': [
120,
0,
0
] // dark red
},
'inactive': {
'nordic': [
200,
200,
200
],
'deep-dark': [
200,
200,
200
],
'material': [
200,
200,
200
], // TODO: Will be calculated by material theme generator
'light': [
65,
65,
65
],
'bright': [
60,
60,
60
],
'test': [
150,
150,
150
]
},
'inactive-background': {
'nordic': [
0,
0,
0
],
'deep-dark': [
0,
0,
0
],
'material': [
255,
255,
255
], // TODO: Will be calculated by material theme generator
'light': [
80,
80,
80
],
'bright': [
60,
60,
60
],
'test': [
60,
60,
60
]
}
};
const fonts = {
'primary': {
'nordic': 'Comfortaa',
'deep-dark': 'Comfortaa',
'material': 'Comfortaa',
'light': 'Adwaita Sans',
'bright': 'Adwaita Sans Extralight'
},
'accent': {
'nordic': 'Adwaita Sans',
'deep-dark': 'Adwaita Sans',
'material': 'Adwaita Sans',
'light': 'Cantarell',
'bright': 'Contarell Thin'
},
'mono': {
'nordic': 'Source Code Pro',
'deep-dark': 'Source Code Pro',
'material': 'Source Code Pro',
'light': 'Jetbrains Mono',
'bright': 'Jetbrains Mono',
}
};
const iconTheme = {
'nordic': 'Candy',
'deep-dark': 'Candy',
'material': 'Candy',
'light': 'Candy',
'bright': 'Candy'
};
const yaziThemes = {
'nordic': 'tokyo-night',
'deep-dark': 'dracula',
'material': 'dracula',
'light': 'catppuccin-latte',
'bright': 'catppuccin-latte',
};
+132
View File
@@ -0,0 +1,132 @@
const mustache = require( 'mustache' );
const colorThief = require( '@janishutz/colorthief' );
const fs = require( 'fs' );
const path = require( 'path' );
const util = require( './util' );
const generateTheme = require( './generateTheme' );
const chalk = require( 'chalk' ).default;
const inquirer = require( 'inquirer' ).default;
const build = ( wallpaper, lockpaper, theme ) => {
console.log( '\n=> Extracting colours' );
// Extract colour palette from chosen wallpaper using Color-Thief
colorThief.getPalette( wallpaper, 20 ).then( palette => {
palette = util.removeUselessColours( palette );
// Define view options (for rendering with mustache)
if ( theme === 'test' ) {
palette = [ [ 255, 0, 0 ], [ 0, 255, 0 ], [ 0, 0, 255 ] ];
}
console.log( 'The following colours will be used based on your wallpaper: ' );
let col = palette[ 0 ];
console.log( ' => Primary accent colour: ' + chalk.rgb( col[ 0 ], col[ 1 ], col[ 2 ] )( util.renderColourAsHex( col ) ) );
col = palette[ 1 ];
console.log( ' => Secondary accent colour: ' + chalk.rgb( col[ 0 ], col[ 1 ], col[ 2 ] )( util.renderColourAsHex( col ) ) );
col = palette[ 2 ];
console.log( ' => Tertiary accent colour: ' + chalk.rgb( col[ 0 ], col[ 1 ], col[ 2 ] )( util.renderColourAsHex( col ) ) );
inquirer.prompt( [{
type: 'confirm',
name: 'confirm-proceed-build',
message: 'Okay to proceed with these colours?'
} ] ).then( answer => {
if ( answer['confirm-proceed-build'] ) proceedWithBuild( wallpaper, lockpaper, theme, palette );
else {
// Have the user pick any other of the extracted colours instead
let counter = -1;
const colourOptions = palette.map( c => {
counter++;
return {
name: chalk.rgb( c[ 0 ], c[ 1 ], c[ 2 ] )( util.renderColourAsHex( c ) ),
value: counter
}
} )
inquirer.prompt( [
{
'type': 'list',
'message': 'Pick the primary accent colour',
'choices': colourOptions,
'name': 'primary'
},
{
'type': 'list',
'message': 'Pick the secondary accent colour',
'choices': colourOptions,
'name': 'secondary'
},
{
'type': 'list',
'message': 'Pick the tertiary accent colour',
'choices': colourOptions,
'name': 'tertiary'
}
] ).then( result => {
const p = [ palette[ result.primary ], palette[ result.secondary ], palette[ result.tertiary ] ];
proceedWithBuild( wallpaper, lockpaper, theme, p );
} ).catch( e => {
console.error( e );
process.exit( 1 );
} );
}
} ).catch( e => {
console.error( e );
process.exit( 1 );
} );
} ).catch( e => {
console.error( e );
console.error( '\n=> Failed to load image or retrieve colour palette from it' );
process.exit( 1 );
} );
}
const proceedWithBuild = ( wallpaper, lockpaper, theme, palette ) => {
const view = generateTheme.generateTheme( theme, wallpaper, lockpaper, palette );
try {
fs.mkdir( path.join( __dirname, '/dist' ) );
} catch ( e ) {
}
// recursively index files from config directory -> Maybe add a file to each
// directory to indicate whether or not to index files in it?
const fileList = util.treeWalker( path.join( __dirname, '/../../renderable/' ), '*', [ 'node_modules', '@girs', '.gitignore', '.git', 'flavours' ] );
for (let index = 0; index < fileList.length; index++) {
try {
render( fileList[ index ], view );
} catch ( e ) {
console.error( '=> Render failed for ' + fileList[ index ] + ' with error ' + e );
}
}
util.themePreProcessor( path.join( __dirname, '/../../gtk-theme/src/gtk-4.0/gtk.css' ), 'src', 'dist' );
util.themePreProcessor( path.join( __dirname, '/../../gtk-theme/src/gtk-3.0/gtk.css' ), 'src', 'dist' );
render( path.join( __dirname, '/../../gtk-theme/src/colours.css' ), view, 'src', 'dist' );
}
/**
* @param {string} templatePath - absolute path to config directory
* @param {object} view - rendering config passed to mustache
* @param {string} originalDir - The original directory, defaults to renderable
* @param {string} newDir - the output directory override, defaults to config
*/
const render = ( templatePath, view, originalDir = 'renderable', newDir = 'config' ) => {
// Load template from disk (all can be found in <project-root>/renderable)
// TODO: Make exclusion better plus copy other files maybe?
const template = '' + fs.readFileSync( templatePath );
const outPath = path.join( templatePath.replace( originalDir, newDir ) );
console.log( '=> Rendering to ' + outPath );
try {
fs.mkdirSync( path.dirname( outPath ), {
recursive: true,
} );
} catch ( e ) {
console.error( e );
}
fs.writeFileSync( outPath, mustache.render( template, view ) );
}
module.exports = build;
+218
View File
@@ -0,0 +1,218 @@
const convert = require( 'color-convert' );
const fs = require( 'fs' );
const path = require( 'path' );
/**
* Recursively find all files with extension in a directory
* @param {string} dir The directory to search. Either absolute or relative path
* @param {string} extension The file extension to look for
* @returns {string[]} returns a list of html files with their full path
*/
const treeWalker = (
dir, extension, ignoreList
) => {
const ls = fs.readdirSync( dir );
const fileList = [];
for ( let file in ls ) {
if ( fs.statSync( path.join( dir, ls[ file ] ) ).isDirectory() ) {
// Filter ignored directories
if ( ignoreList === undefined || !ignoreList.includes( ls[ file ] ) ) {
const newFiles = treeWalker(
path.join( dir, ls[ file ] ), extension, ignoreList
);
for ( let file = 0; file < newFiles.length; file++ ) {
fileList.push( newFiles[ file ] );
}
}
} else if ( extension == '*' || ls[ file ].includes( extension ) ) {
if ( ignoreList === undefined || !ignoreList.includes( ls[ file ] ) ) {
fileList.push( path.join( dir, ls[ file ] ) );
}
}
}
return fileList;
};
const renderColourAsHex = colour => {
return '#' + convert.default.rgb.hex(
colour[ 0 ], colour[ 1 ], colour[ 2 ]
);
};
const renderColourAsRGB = colour => {
return `rgb(${ colour[ 0 ] }, ${ colour[ 1 ] }, ${ colour[ 2 ] })`;
};
const renderColourAsRGBA = ( colour, ambiance ) => {
return `rgba(${ colour[ 0 ] }, ${ colour[ 1 ] }, ${ colour[ 2 ] }, ${ ambiance })`;
};
const renderColourAsRGBHex = colour => {
const hexCol = convert.default.rgb.hex(
colour[ 0 ], colour[ 1 ], colour[ 2 ]
);
return `rgb(${ hexCol })`.toLowerCase();
};
function decimalToHex ( decimal ) {
const hexValue = Math.round( decimal * 255 );
return hexValue.toString( 16 ).padStart( 2, '0' );
}
const renderColourAsRGBAHex = ( colour, ambiance ) => {
const hexCol = convert.default.rgb.hex(
colour[ 0 ], colour[ 1 ], colour[ 2 ]
);
return `rgba(${ hexCol }${ decimalToHex( ambiance ) })`.toLowerCase();
};
const removeUselessColours = palette => {
const p = [];
for ( let i = 0; i < palette.length; i++ ) {
const el = palette[ i ];
const luminance = calculateLuminance( palette[ i ] );
if ( luminance < 210 && luminance > 40 ) {
p.push( palette[ i ] );
}
for ( let j = 0; j < el.length; j++ ) {
if ( el[j] > 70 ) {
p.push( palette[ i ] );
break;
}
}
}
return p;
};
const calculateLuminance = colour => {
return colour[ 0 ] + colour[ 1 ] + colour[ 2 ] / 3;
};
/*
* Replace the colours with variable names
*/
const replacements = {
'#0f1011': '@bg',
'rgba(9, 9, 10, 0.9)': '@bg_rgba_07',
'rgba(26, 28, 30, 0.3)': '@bg_rgba_05',
'#000': '@bg_accent',
'#000000': '@bg_accent',
'rgba(0, 0, 0, 0.7)': '@bg_accent_rgba_07',
'rgba(0, 0, 0, 0.6)': '@bg_accent_rgba_06',
'rgba(0, 0, 0, 0.5)': '@bg_accent_rgba_05',
'rgba(0, 0, 0, 0.4)': '@bg_accent_rgba_04',
'rgba(0, 0, 0, 0.3)': '@bg_accent_rgba_03',
'rgba(0, 0, 0, 0.12)': '@bg_accent_rgba_015',
'rgba(0, 0, 0, 0.08)': '@bg_accent_rgba_01',
'rgba(9, 9, 10, 0.9)': '@bg_inactive',
'#80868b': '@inactive',
'rgba(128, 134, 139, 0.7)': '@inactive_rgba_07',
'rgba(128, 134, 139, 0.5)': '@inactive_rgba_05',
'rgba(128, 134, 139, 0.3)': '@inactive_rgba_03',
'rgba(128, 134, 139, 0.2)': '@inactive_rgba_02',
// '#555A': '@shadow_rgba',
// '#555': '@shadow',
'#387db7': '@accent',
'rgba(56, 125, 183, 0.5)': '@accent_rgba_05',
'rgba(56, 125, 183, 0.32)': '@accent_rgba_03',
'rgba(56, 125, 183, 0.24)': '@accent_rgba_02',
'rgba(56, 125, 183, 0.16)': '@accent_rgba_015',
'rgba(56, 125, 183, 0.12)': '@accent_rgba_011',
'rgba(56, 125, 183, 0.08)': '@accent_rgba_007',
'#1a1a1b': '@accent_gradient_5',
'#1f1f21': '@accent_gradient_4',
'#1a2530': '@accent_gradient_3',
'#1c2c3b': '@accent_gradient_2',
'#1e3040': '@accent_gradient_1',
'#4887bd': '@accent_gradient_inverse_1',
'#508dc0': '@accent_gradient_inverse_2',
'#5892c3': '@accent_gradient_inverse_3',
'#673ab7': '@accent2',
'rgba(103, 58, 183, 0.12)': '@accent2_rgba_015',
'#fff': '@fg_accent',
'rgba(255, 255, 255, 0.7)': '@fg_accent_rgba_07',
'rgba(255, 255, 255, 0.6)': '@fg_accent_rgba_06',
'rgba(255, 255, 255, 0.5)': '@fg_accent_rgba_05',
'rgba(255, 255, 255, 0.3)': '@fg_accent_rgba_03',
'rgba(255, 255, 255, 0.2)': '@fg_accent_rgba_02',
'#9e9e9e': '@fg',
'rgba(158, 158, 158, 0.7)': '@fg_rgba_07',
'rgba(158, 158, 158, 0.6)': '@fg_rgba_06',
'rgba(158, 158, 158, 0.5)': '@fg_rgba_05',
'rgba(158, 158, 158, 0.3)': '@fg_rgba_03',
'rgba(158, 158, 158, 0.2)': '@fg_rgba_02',
'rgba(158, 158, 158, 0.1168)': '@fg_rgba_01'
};
const themePreProcessor = (
file, replacement, out
) => {
const colours = Object.keys( replacements );
let data = '' + fs.readFileSync( file );
for ( let index = 0; index < colours.length; index++ ) {
const colour = colours[index];
data = data.replaceAll( colour, replacements[ colour ] );
}
const outPath = file.replace( replacement, out );
try {
fs.mkdirSync( path.dirname( outPath ), {
'recursive': true,
} );
} catch ( e ) {
console.error( e );
}
fs.writeFileSync( outPath, data );
};
const getGradientColour = (
colour, index, multiplier
) => {
if ( index === 0 ) {
return [
colour[ 0 ] * multiplier,
colour[ 1 ] * multiplier,
colour[ 2 ] * multiplier
];
}
const gradient = getGradientColour(
colour, index - 1, multiplier
);
return [
Math.min( 255, gradient[ 0 ] * multiplier ),
Math.min( 255, gradient[ 1 ] * multiplier ),
Math.min( 255, gradient[ 2 ] * multiplier )
];
};
module.exports = {
treeWalker,
renderColourAsHex,
renderColourAsRGB,
renderColourAsRGBA,
renderColourAsRGBHex,
renderColourAsRGBAHex,
themePreProcessor,
getGradientColour,
removeUselessColours
};
+23
View File
@@ -0,0 +1,23 @@
{
"name": "janishutz-config-build",
"version": "1.0.0",
"description": "Build janishutz's dotfiles configs",
"repository": {
"type": "git",
"url": "https://git.janishutz.com/janishutz/dotfiles"
},
"license": "GPL-3.0-or-later",
"author": "janishutz",
"type": "commonjs",
"main": "build.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@janishutz/colorthief": "^3.0.2",
"chalk": "^5.4.1",
"color-convert": "^3.0.1",
"inquirer": "^12.5.0",
"mustache": "^4.2.0"
}
}
+50
View File
@@ -0,0 +1,50 @@
const fs = require( 'fs' );
const path = require( 'path' );
const data = '' + fs.readFileSync( '/usr/share/themes/Material-Black-Blueberry/gtk-4.0/gtk.css' );
let lineNumber = 1;
const indexer = {};
for ( let i = 0; i < data.length; i++ ) {
const char = data[i];
if ( char === '\n' ) {
lineNumber++;
} else if ( char === '#' ) {
const extract = data.substring( i );
const col = extract.slice( 0, extract.indexOf( '\n' ) );
if ( !indexer[ col ] ) {
indexer[ col ] = [];
}
indexer[ col ].push( lineNumber );
} else if ( char === 'r' ) {
const extract = data.substring( i );
if ( extract.slice( 0, 3 ) === 'rgb' ) {
const col = extract.slice( 0, extract.indexOf( '\n' ) );
if ( !indexer[ col ] ) {
indexer[ col ] = [];
}
indexer[ col ].push( lineNumber );
}
}
}
// Output
const keys = Object.keys( indexer );
for ( let i = 0; i < keys.length; i++ ) {
const element = keys[i];
if ( element.length <= 25 ) {
console.log( 'Colour ' + element.slice( 0, element.length - 1 ) + ' appears on ' + indexer[ element ] );
}
}
-10
View File
@@ -1,10 +0,0 @@
grub_theme = {
"desktop": "/usr/share/grub/themes/monterey-grub-theme/theme.txt",
"laptop": "/usr/share/grub/themes/monterey-grub-theme/theme.txt",
}
grub_os_prober = {"desktop": True, "laptop": False}
kernel_args_all = {"desktop": "", "laptop": ""}
kernel_args_debug_only = {"desktop": "", "laptop": ""}
kernel_args_normal_only = {"desktop": "", "laptop": ""}
user = "janis"
+131
View File
@@ -0,0 +1,131 @@
; thunar GtkAccelMap rc-file -*- scheme -*-
; this file is an automated accelerator map dump
;
; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-type" "")
; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-last-modified" "")
; (gtk_accel_path "<Actions>/Thunarwindow/menu" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/cut" "<Primary>x")
; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-size" "")
; (gtk_accel_path "<Actions>/ThunarWindow/file-menu" "")
; (gtk_accel_path "<Actions>/ThunarBookmarks/e92146de8ffbb74bfd99784ed40bdeef" "")
; (gtk_accel_path "<Actions>/ThunarWindow/close-tab" "<Primary>w")
; (gtk_accel_path "<Actions>/ThunarWindow/switch-previous-tab-alt" "<Primary><Shift>ISO_Left_Tab")
; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-size" "")
; (gtk_accel_path "<Actions>/ThunarWindow/new-window" "<Primary>n")
; (gtk_accel_path "<Actions>/ThunarWindow/clear-directory-specific-settings" "")
; (gtk_accel_path "<Actions>/ThunarWindow/close-window" "<Primary>q")
; (gtk_accel_path "<Actions>/ThunarWindow/open-parent" "<Alt>Up")
; (gtk_accel_path "<Actions>/ThunarWindow/view-side-pane-menu" "")
; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-size-in-bytes" "")
; (gtk_accel_path "<Actions>/ThunarWindow/switch-previous-tab" "<Primary>Page_Up")
; (gtk_accel_path "<Actions>/ThunarActionManager/open" "<Primary>o")
; (gtk_accel_path "<Actions>/ThunarStandardView/sort-ascending" "")
; (gtk_accel_path "<Actions>/ThunarWindow/toggle-split-view" "F3")
; (gtk_accel_path "<Actions>/ThunarActionManager/copy-2" "<Primary>Insert")
; (gtk_accel_path "<Actions>/ThunarActionManager/trash-delete" "Delete")
; (gtk_accel_path "<Actions>/ThunarWindow/view-configure-toolbar" "")
; (gtk_accel_path "<Actions>/ThunarWindow/open-recent" "")
; (gtk_accel_path "<Actions>/ThunarBookmarks/7a203ca58c14c76ec1b07d08eaba7e8a" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/forward" "<Alt>Right")
; (gtk_accel_path "<Actions>/ThunarActionManager/restore" "")
; (gtk_accel_path "<Actions>/ThunarWindow/open-location-alt" "<Alt>d")
; (gtk_accel_path "<Actions>/ThunarStandardView/select-by-pattern" "<Primary>s")
; (gtk_accel_path "<Actions>/ThunarWindow/zoom-out-alt" "<Primary>KP_Subtract")
; (gtk_accel_path "<Actions>/ThunarWindow/contents" "F1")
; (gtk_accel_path "<Actions>/ThunarWindow/open-file-menu" "F10")
; (gtk_accel_path "<Actions>/ThunarWindow/show-highlight" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/sort-descending" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-name" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/select-all-files" "<Primary>a")
; (gtk_accel_path "<Actions>/ThunarActionManager/execute" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/properties" "<Alt>Return")
; (gtk_accel_path "<Actions>/ThunarActionManager/cut-2" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-dtime" "")
; (gtk_accel_path "<Actions>/ThunarBookmarks/de6d2099b95ef15820d04f5c3a6dc0f8" "")
; (gtk_accel_path "<Actions>/ThunarWindow/open-templates" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/paste-2" "<Shift>Insert")
; (gtk_accel_path "<Actions>/ThunarWindow/switch-next-tab" "<Primary>Page_Down")
; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-filetype" "")
; (gtk_accel_path "<Actions>/ThunarWindow/close-all-windows" "<Primary><Shift>w")
; (gtk_accel_path "<Actions>/ThunarStandardView/create-document" "")
; (gtk_accel_path "<Actions>/ThunarWindow/detach-tab" "")
; (gtk_accel_path "<Actions>/ThunarWindow/cancel-search" "Escape")
; (gtk_accel_path "<Actions>/ThunarWindow/zoom-in-alt2" "<Primary>equal")
; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-hidden-count" "")
; (gtk_accel_path "<Actions>/ThunarShortcutsPane/sendto-shortcuts" "<Primary>d")
; (gtk_accel_path "<Actions>/ThunarActionManager/undo" "<Primary>z")
; (gtk_accel_path "<Actions>/ThunarBookmarks/0064c8b8c2b8ae1954479b6f2feab576" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/toggle-sort-order" "")
; (gtk_accel_path "<Actions>/ThunarWindow/view-location-selector-entry" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/paste" "<Primary>v")
; (gtk_accel_path "<Actions>/ThunarWindow/zoom-in-alt1" "<Primary>KP_Add")
; (gtk_accel_path "<Actions>/ThunarWindow/view-menubar" "<Primary>m")
; (gtk_accel_path "<Actions>/ThunarStandardView/back" "<Alt>Left")
; (gtk_accel_path "<Actions>/ThunarWindow/open-desktop" "")
; (gtk_accel_path "<Actions>/ThunarWindow/view-as-detailed-list" "<Primary>2")
; (gtk_accel_path "<Actions>/ThunarActionManager/restore-show" "")
; (gtk_accel_path "<Actions>/ThunarWindow/sendto-menu" "")
; (gtk_accel_path "<Actions>/ThunarStatusBar/toggle-display-name" "")
; (gtk_accel_path "<Actions>/ThunarWindow/go-menu" "")
; (gtk_accel_path "<Actions>/ThunarWindow/zoom-out" "<Primary>minus")
; (gtk_accel_path "<Actions>/ThunarWindow/remove-from-recent" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/open-with-other" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/invert-selection" "<Primary><Shift>i")
; (gtk_accel_path "<Actions>/ThunarWindow/view-side-pane-shortcuts" "<Primary>b")
; (gtk_accel_path "<Actions>/ThunarWindow/reload-alt-2" "Reload")
; (gtk_accel_path "<Actions>/ThunarBookmarks/fb8a2fb8c18b440d557134b69e8bfc79" "")
; (gtk_accel_path "<Actions>/ThunarWindow/view-location-selector-menu" "")
; (gtk_accel_path "<Actions>/ThunarWindow/edit-menu" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/copy" "<Primary>c")
; (gtk_accel_path "<Actions>/ThunarStandardView/sort-by-mtime" "")
; (gtk_accel_path "<Actions>/ThunarWindow/reload-alt-1" "F5")
; (gtk_accel_path "<Actions>/ThunarStandardView/forward-alt" "Forward")
; (gtk_accel_path "<Actions>/ThunarActionManager/move-to-trash" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/delete-3" "<Shift>KP_Delete")
; (gtk_accel_path "<Actions>/ThunarWindow/bookmarks-menu" "")
; (gtk_accel_path "<Actions>/ThunarWindow/reload" "<Primary>r")
; (gtk_accel_path "<Actions>/ThunarBookmarks/ab554ce947264f765ed1ba66309937f1" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/arrange-items-menu" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/unselect-all-files" "Escape")
; (gtk_accel_path "<Actions>/ThunarWindow/open-computer" "")
; (gtk_accel_path "<Actions>/ThunarWindow/toggle-image-preview" "")
; (gtk_accel_path "<Actions>/ThunarWindow/toggle-side-pane" "F9")
; (gtk_accel_path "<Actions>/ThunarWindow/view-as-icons" "<Primary>1")
; (gtk_accel_path "<Actions>/ThunarActionManager/delete-2" "<Shift>Delete")
; (gtk_accel_path "<Actions>/ThunarWindow/zoom-in" "<Primary>plus")
; (gtk_accel_path "<Actions>/ThunarStandardView/rename" "F2")
; (gtk_accel_path "<Actions>/ThunarWindow/open-location" "<Primary>l")
; (gtk_accel_path "<Actions>/ThunarWindow/view-as-compact-list" "<Primary>3")
; (gtk_accel_path "<Actions>/ThunarWindow/view-menu" "")
; (gtk_accel_path "<Actions>/ThunarWindow/search" "<Primary>f")
; (gtk_accel_path "<Actions>/ThunarWindow/new-tab" "<Primary>t")
; (gtk_accel_path "<Actions>/ThunarWindow/zoom-reset" "<Primary>0")
; (gtk_accel_path "<Actions>/ThunarWindow/contents/help-menu" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/open-in-new-tab" "<Primary><Shift>p")
; (gtk_accel_path "<Actions>/ThunarWindow/view-location-selector-buttons" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/back-alt2" "Back")
; (gtk_accel_path "<Actions>/ThunarActionManager/redo" "<Primary><Shift>z")
; (gtk_accel_path "<Actions>/ThunarWindow/open-trash" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/open-in-new-window" "<Primary><Shift>o")
; (gtk_accel_path "<Actions>/ThunarWindow/view-statusbar" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/open-location" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/duplicate" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/trash-delete-2" "KP_Delete")
; (gtk_accel_path "<Actions>/ThunarStandardView/back-alt1" "BackSpace")
; (gtk_accel_path "<Actions>/ThunarStandardView/create-folder" "<Primary><Shift>n")
; (gtk_accel_path "<Actions>/ThunarWindow/open-home" "<Alt>Home")
; (gtk_accel_path "<Actions>/ThunarWindow/switch-focused-split-view-pane" "")
; (gtk_accel_path "<Actions>/ThunarWindow/show-hidden" "<Primary>h")
; (gtk_accel_path "<Actions>/ThunarStandardView/set-default-app" "")
; (gtk_accel_path "<Actions>/ThunarWindow/empty-trash" "")
; (gtk_accel_path "<Actions>/ThunarWindow/preferences" "")
; (gtk_accel_path "<Actions>/ThunarActionManager/delete" "")
; (gtk_accel_path "<Actions>/ThunarWindow/open-network" "")
; (gtk_accel_path "<Actions>/ThunarWindow/view-side-pane-tree" "<Primary>e")
; (gtk_accel_path "<Actions>/ThunarWindow/open-file-system" "")
; (gtk_accel_path "<Actions>/ThunarWindow/search-alt" "Search")
; (gtk_accel_path "<Actions>/ThunarWindow/switch-next-tab-alt" "<Primary>Tab")
; (gtk_accel_path "<Actions>/ThunarActionManager/sendto-desktop" "")
; (gtk_accel_path "<Actions>/ThunarStandardView/make-link" "")
; (gtk_accel_path "<Actions>/ThunarWindow/zoom-reset-alt" "<Primary>KP_0")
; (gtk_accel_path "<Actions>/ThunarWindow/about" "")
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<actions>
<action>
<icon>utilities-terminal</icon>
<name>Open Terminal Here</name>
<submenu></submenu>
<unique-id>1675076590898177-1</unique-id>
<command>terminator --working-directory %f</command>
<description>Example for a custom action</description>
<range></range>
<patterns>*</patterns>
<startup-notify/>
<directories/>
</action>
<action>
<icon>com.github.artemanufrij.findfileconflicts</icon>
<name>Fix filenames</name>
<submenu></submenu>
<unique-id>1691249342110327-1</unique-id>
<command>/home/janis/scripts/removeSpaces.sh %f</command>
<description>A script that fixes the filenames in a directory recursively</description>
<range>*</range>
<patterns>*</patterns>
</action>
</actions>
+2
View File
@@ -0,0 +1,2 @@
node_modules/
@girs/
+19
View File
@@ -0,0 +1,19 @@
import { App } from "astal/gtk4"
import style from "./style.scss"
import Bar from "./ui/Launcher"
App.start({
css: style,
main() {
App.get_monitors().map(Bar)
},
requestHandler(request, res) {
if ( request === 'open' ) {
res( 'ok' );
} else if ( request === 'close' ) {
res( 'ok' );
} else if ( request === 'toggle' ) {
res( 'ok' );
}
}
})
+75
View File
@@ -0,0 +1,75 @@
/*
* dotfiles - components.d.ts
*
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import type { UIComponent, ResultElement } from "./rendering";
export interface App extends ResultElement {
/**
* The app start command that will be executed
*/
command: string;
}
// TODO: Finish
export interface DictionaryEntry extends ResultElement {
/**
* Execute no command
*/
action: null;
/**
* The dictionary definition
*/
definition: string;
}
export interface CMDOutput extends ResultElement {
/**
* Stdout from the command that was run
*/
result: string;
}
export interface Calculation extends ResultElement {
/**
* THe calculation result
*/
result: string;
}
/* ************* *
* UI Components *
* ************* */
export interface LargeUIComponent extends UIComponent {
/**
* The number of items to display per line. Image size will automatically be scaled
* based on width
*/
itemsPerLine: number;
}
export interface MediumUIComponent extends UIComponent {}
export interface ListUIComponent extends UIComponent {}
export interface DictionaryUIComponent extends UIComponent {
elements: DictionaryEntry[];
}
export interface CMDOutputUIComponent extends UIComponent {
elements: CMDOutput[];
}
export interface CalculationUIComponent extends UIComponent {
elements: Calculation[];
}
+60
View File
@@ -0,0 +1,60 @@
/*
* dotfiles - rendering.d.ts
*
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
export interface UIComponent {
/**
* The title of the component (like a category name), shown above small divider line
*/
title: string;
/**
* ResultElement list, made up of all elements that should be shown
*/
elements: ResultElement[];
/**
* Choose how many elements to show before truncating (will expand when command is run)
*/
truncate: number;
/**
* The weight of the element (determines order)
*/
weight: number;
}
export interface ResultElement {
/**
* The name of the result element
*/
name: string;
/**
* Path to the image to be displayed in the UI
*/
img: string;
/**
* The weight of the element (determines order)
*/
weight: number;
/**
* The action to be executed
*/
action: Action;
/**
* The font size of the text (optional)
*/
fontSize: number | undefined;
}
type Action = '' | null;
+21
View File
@@ -0,0 +1,21 @@
declare const SRC: string
declare module "inline:*" {
const content: string
export default content
}
declare module "*.scss" {
const content: string
export default content
}
declare module "*.blp" {
const content: string
export default content
}
declare module "*.css" {
const content: string
export default content
}
+32
View File
@@ -0,0 +1,32 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fast_fuzzy_1 = require("fast-fuzzy");
const fs_1 = __importDefault(require("fs"));
// Get list source from args
// ARGS: type source
// Then we read query from stdin to not restart indexing & the like for each keystroke
let data = [];
if (process.argv[2] === 'fs') {
if (process.argv[3].includes('.json')) {
data = JSON.parse('' + fs_1.default.readFileSync(process.argv[3]));
}
else if (process.argv[3].includes('.txt')) {
data = ('' + fs_1.default.readFileSync(process.argv[3])).split(',');
}
else if (fs_1.default.statSync(process.argv[3]).isDirectory()) {
data = fs_1.default.readdirSync(process.argv[3]);
}
}
else if (process.argv[2] === 'arg') {
data = process.argv[3].split(',');
}
else {
throw new Error('Invalid argument at position 1. Can be either fs or arg, not ' + process.argv[2]);
}
process.stdin.on("data", (query) => {
// On stdin submit (which the other client will have to support) process data
console.log((0, fast_fuzzy_1.search)(query.toString(), data));
});
@@ -0,0 +1,18 @@
{
"name": "fzf",
"version": "1.0.0",
"description": "",
"license": "ISC",
"author": "",
"type": "commonjs",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@types/node": "^22.14.0"
},
"dependencies": {
"fast-fuzzy": "^1.12.0"
}
}
@@ -0,0 +1,27 @@
import { search } from 'fast-fuzzy';
import fs from 'fs';
// Get list source from args
// ARGS: type source
// Then we read query from stdin to not restart indexing & the like for each keystroke
let data: string[] = [];
if ( process.argv[ 2 ] === 'fs' ) {
if ( process.argv[ 3 ].includes( '.json' ) ) {
data = JSON.parse( '' + fs.readFileSync( process.argv[ 3 ] ) );
} else if ( process.argv[ 3 ].includes( '.txt' ) ) {
data = ( '' + fs.readFileSync( process.argv[ 3 ] ) ).split( ',' );
} else if ( fs.statSync( process.argv[ 3 ] ).isDirectory() ) {
data = fs.readdirSync( process.argv[ 3 ] );
}
} else if ( process.argv[ 2 ] === 'arg' ) {
data = process.argv[ 3 ].split( ',' );
} else {
throw new Error( 'Invalid argument at position 1. Can be either fs or arg, not ' + process.argv[ 2 ] );
}
process.stdin.on( "data", ( query ) => {
// On stdin submit (which the other client will have to support) process data
console.log( search( query.toString(), data ) );
} );
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": true,
"target": "ES6",
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"types": ["node"],
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": [ "./src/**/*" ],
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "astal-shell",
"dependencies": {
"astal": "/usr/share/astal/gjs"
}
}
+20
View File
@@ -0,0 +1,20 @@
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
$fg-color: #{"@theme_fg_color"};
$bg-color: #{"@theme_bg_color"};
window.Bar {
background: transparent;
color: $fg-color;
font-weight: bold;
>centerbox {
background: $bg-color;
border-radius: 10px;
margin: 8px;
}
button {
border-radius: 8px;
margin: 2px;
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"experimentalDecorators": true,
"strict": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
// "checkJs": true,
// "allowJs": true,
"jsx": "react-jsx",
"jsxImportSource": "astal/gtk4",
}
}
+32
View File
@@ -0,0 +1,32 @@
import { App, Astal, Gtk, Gdk } from "astal/gtk4"
import { Variable } from "astal"
function hide() {
App.get_window("launcher")!.hide()
}
export default function Launcher(monitor: Gdk.Monitor) {
const { CENTER } = Gtk.Align
const width = Variable(1000)
const text = Variable("")
return <window
name="launcher"
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM}
exclusivity={Astal.Exclusivity.IGNORE}
keymode={Astal.Keymode.ON_DEMAND}
application={App}
onShow={(self) => {
text.set("")
width.set(self.get_current_monitor().get_width_mm())
}}
onKeyPressed={(self, keyval, keycode) => {
print( 'key pressed: ' + keyval + ' which has code ' + keycode );
}}>
<box>
</box>
</window>
}
@@ -0,0 +1,3 @@
export default () => {
return <box></box>
}
+12
View File
@@ -0,0 +1,12 @@
/*
* dotfiles - file.ts
*
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { readFileAsync, writeFileAsync, monitorFile } from "astal";
+24
View File
@@ -0,0 +1,24 @@
/*
* dotfiles - fzf.ts
*
* Created by Janis Hutz 03/30/2025, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import AstalApps from "gi://AstalApps?version=0.1"
// TODO: For all astal apps, read a global colours config file
const fzfApplication = ( query: string ) => {
const apps = new AstalApps.Apps()
return apps.fuzzy_query( query );
}
const fzfCmd = ( query: string ) => {
}
export default {
fzfApplication
}
+51
View File
@@ -0,0 +1,51 @@
/*
* dotfiles - search.ts
*
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import subprocessRunner from "./subprocessRunner";
import fzf from "./fzf";
const preprocess = ( input: string ) => {
// Find out what kind of instruction to process
if ( input.startsWith( ':' ) ) {
processCommand( input.substring( 1, input.indexOf( ' ' ) ), input.substring( input.indexOf( ' ' ) ).split( ' ' ) );
} else if ( input.startsWith( '!' ) ) {
processBang( input.substring( 1, input.indexOf( ' ' ) ), input.substring( input.indexOf( ' ' ) ) );
} else {
// Determine if entered string is calculation or not
// We can easily do that by asking qalc (qalculate cli) if this is fine
subprocessRunner.executeCommand( 'qalc "' + input + '"' ).then( out => {
// we get a calculation result here
print( out );
processCalculation( out );
} ).catch( err => {
processSearch( input );
print( err );
} );
}
}
const processSearch = ( input: string ) => {
}
const processCalculation = ( output: string ) => {
}
const processCommand = ( cmd: string, args: string[] ) => {
}
const processBang = ( bang: string, input: string ) => {
}
export default {
preprocess
}
@@ -0,0 +1,43 @@
/*
* dotfiles - subprocessRunner.ts
*
* Created by Janis Hutz 03/22/2025, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { subprocess, execAsync, Process } from "astal/process";
// TODO: Get cwd and the likes to then use that to run JavaScript files with node / python with python, etc
/**
* Run a subprocess. If you simply want to run a command that doesn't need continuous updates
* run executeCommand instead.
* @param cmd - The command to be run
* @param onOut - Calback function for stdout of the subprocess
* @param onErr - [TODO:description]
* @returns [TODO:return]
*/
const startSubProcess = (
cmd: string | string[],
onOut: (stdout: string) => void,
onErr: (stderr: string) => void | undefined,
): Process => {
return subprocess( cmd, onOut, onErr );
};
/**
* Run a command. If you need continuous updates, run startSubProcess instead
* @param cmd - The command to be run. Either a string or an array of strings
* @returns A Promise resolving to stdout of the command
*/
const executeCommand = (cmd: string | string[]): Promise<string> => {
return execAsync( cmd );
};
export default {
startSubProcess,
executeCommand
}
+2
View File
@@ -0,0 +1,2 @@
node_modules/
@girs/
+32
View File
@@ -0,0 +1,32 @@
import { App } from "astal/gtk3"
import style from "./style.scss"
import not from "./handler"
App.start({
instanceName: "notifier",
css: style,
main() {
not.startNotificationHandler( 0, App.get_monitors()[0] )
},
requestHandler(request, res) {
if ( request == 'show' ) {
not.openNotificationMenu( 0 );
res( 'Showing all open notifications' );
} else if ( request == 'hide' ) {
not.closeNotificationMenu( 0 );
res( 'Hid all notifications' );
} else if ( request == 'clear' ) {
not.clearAllNotifications( 0 );
res( 'Cleared all notifications' );
} else if ( request == 'clear-newest' ) {
not.clearNewestNotifications( 0 );
res( 'Cleared newest notification' );
} else if ( request == 'toggle' ) {
not.toggleNotificationMenu( 0 );
res( 'Toggled notifications' );
} else {
res( 'Unknown command!' );
}
},
})
+21
View File
@@ -0,0 +1,21 @@
declare const SRC: string
declare module "inline:*" {
const content: string
export default content
}
declare module "*.scss" {
const content: string
export default content
}
declare module "*.blp" {
const content: string
export default content
}
declare module "*.css" {
const content: string
export default content
}
+206
View File
@@ -0,0 +1,206 @@
/*
* dotfiles - handler.ts
*
* Created by Janis Hutz 03/21/2025, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
import { Astal, Gtk, Gdk } from "astal/gtk3"
import Notifd from "gi://AstalNotifd";
import Notification from "./notifications/notifications";
import { type Subscribable } from "astal/binding";
import { Variable, bind, timeout } from "astal"
// Config
const TIMEOUT_DELAY = 5000;
class Notifier implements Subscribable {
private display: Map<number, Gtk.Widget> = new Map();
private notifications: Map<number, Notifd.Notification> = new Map();
private notifd: Notifd.Notifd;
private subscriberData: Variable<Gtk.Widget[]> = Variable( [] );
private instanceID: number;
private menuOpen: boolean;
/**
* Sets up the notifier
*/
constructor( id: number ) {
this.instanceID = id;
this.menuOpen = false;
this.notifd = Notifd.get_default();
this.notifd.ignoreTimeout = true;
this.notifd.connect( 'notified', ( _, id ) => {
this.add( id );
} );
this.notifd.connect( 'resolved', ( _, id ) => {
this.delete( id );
} );
}
private notify () {
this.subscriberData.set( [ ...this.display.values() ].reverse() );
}
private add ( id: number ) {
this.notifications.set( id, this.notifd.get_notification( id )! );
this.show( id );
}
/**
* Show an element on screen
* @param id - The id of the element to be shown
*/
private show ( id: number ) {
this.set( id, Notification( {
notification: this.notifications.get( id )!,
onHoverLost: () => {
if ( !this.menuOpen ) {
this.hide( id );
}
},
setup: () => timeout( TIMEOUT_DELAY, () => {
if ( !this.menuOpen ) {
this.hide( id );
}
} ),
id: id,
delete: deleteHelper,
instanceID: this.instanceID
} ) )
}
/**
* Set a selected widget to be shown
* @param id - The id of the element to be referenced for later
* @param widget - A GTK widget instance
*/
private set ( id: number, widget: Gtk.Widget ) {
this.display.get( id )?.destroy();
this.display.set( id, widget );
this.notify();
}
/**
* Hide, not delete notification (= send to notification centre)
* @param id - The id of the notification to hide
*/
private hide ( id: number ) {
this.display.get( id )?.destroy();
this.display.delete( id );
this.notify();
}
/**
* Delete a notification (from notification centre too)
* @param id - The id of the notification to hide
*/
delete ( id: number ) {
this.hide( id );
this.notifications.get( id )?.dismiss();
this.notifications.delete( id );
if ( this.notifications.size == 0 ) {
this.menuOpen = false;
}
}
openNotificationMenu () {
// Show all notifications that have not been cleared
if ( this.notifications.size > 0 ) {
this.menuOpen = true;
this.notifications.forEach( ( _, id ) => {
this.show( id );
} )
}
}
hideNotifications () {
this.menuOpen = false;
this.notifications.forEach( ( _, id ) => {
this.hide( id );
} );
}
toggleNotificationMenu () {
if ( this.menuOpen ) {
this.hideNotifications();
} else {
this.openNotificationMenu();
}
}
clearAllNotifications () {
this.menuOpen = false;
this.notifications.forEach( ( _, id ) => {
this.delete( id );
} )
}
clearNewestNotification () {
this.delete( [ ...this.notifications.keys() ][0] );
}
subscribe(callback: (value: unknown) => void): () => void {
return this.subscriberData.subscribe( callback );
}
get() {
return this.subscriberData.get();
}
}
const notifiers: Map<number, Notifier> = new Map();
const deleteHelper = ( id: number, instanceID: number ) => {
notifiers.get( instanceID )?.delete( id );
}
const openNotificationMenu = ( id: number ) => {
notifiers.get( id )?.openNotificationMenu();
}
const closeNotificationMenu = ( id: number ) => {
notifiers.get( id )?.hideNotifications();
}
const toggleNotificationMenu = ( id: number ) => {
notifiers.get( id )?.toggleNotificationMenu();
}
const clearAllNotifications = ( id: number ) => {
notifiers.get( id )?.clearAllNotifications();
}
const clearNewestNotifications = ( id: number ) => {
notifiers.get( id )?.clearNewestNotification();
}
const startNotificationHandler = (id: number, gdkmonitor: Gdk.Monitor) => {
const { TOP, RIGHT } = Astal.WindowAnchor
const notifier: Notifier = new Notifier( id );
notifiers.set( id, notifier );
return <window
className="NotificationHandler"
gdkmonitor={gdkmonitor}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | RIGHT}>
<box vertical noImplicitDestroy>
{bind(notifier)}
</box>
</window>
}
export default {
startNotificationHandler,
openNotificationMenu,
closeNotificationMenu,
clearAllNotifications,
clearNewestNotifications,
toggleNotificationMenu
}
@@ -0,0 +1,125 @@
@use "sass:string";
@function gtkalpha($c, $a) {
@return string.unquote("alpha(#{$c},#{$a})");
}
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
$fg-color: #{"@theme_fg_color"};
$bg-color: #{"@theme_bg_color"};
$error: red;
window.NotificationPopups {
all: unset;
}
eventbox.Notification {
&:first-child>box {
margin-top: 1rem;
}
&:last-child>box {
margin-bottom: 1rem;
}
// eventboxes can not take margins so we style its inner box instead
>box {
min-width: 400px;
border-radius: 13px;
background-color: $bg-color;
margin: .5rem 1rem .5rem 1rem;
box-shadow: 2px 3px 8px 0 gtkalpha(black, .4);
border: 1pt solid gtkalpha($fg-color, .03);
}
&.critical>box {
border: 1pt solid gtkalpha($error, .4);
.header {
.app-name {
color: gtkalpha($error, .8);
}
.app-icon {
color: gtkalpha($error, .6);
}
}
}
.header {
padding: .5rem;
color: gtkalpha($fg-color, 0.5);
.app-icon {
margin: 0 .4rem;
}
.app-name {
margin-right: .3rem;
font-weight: bold;
&:first-child {
margin-left: .4rem;
}
}
.time {
margin: 0 .4rem;
}
button {
padding: .2rem;
min-width: 0;
min-height: 0;
}
}
separator {
margin: 0 .4rem;
background-color: gtkalpha($fg-color, .1);
}
.content {
margin: 1rem;
margin-top: .5rem;
.summary {
font-size: 1.2em;
color: $fg-color;
}
.body {
color: gtkalpha($fg-color, 0.8);
}
.image {
border: 1px solid gtkalpha($fg-color, .02);
margin-right: .5rem;
border-radius: 9px;
min-width: 100px;
min-height: 100px;
background-size: cover;
background-position: center;
}
}
.actions {
margin: 1rem;
margin-top: 0;
button {
margin: 0 .3rem;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
}
@@ -0,0 +1,112 @@
// From astal examples
import { GLib } from "astal"
import { Gtk, Astal } from "astal/gtk3"
import { type EventBox } from "astal/gtk3/widget"
import Notifd from "gi://AstalNotifd"
const isIcon = (icon: string) =>
!!Astal.Icon.lookup_icon(icon)
const fileExists = (path: string) =>
GLib.file_test(path, GLib.FileTest.EXISTS)
const time = (time: number, format = "%H:%M") => GLib.DateTime
.new_from_unix_local(time)
.format(format)!
const urgency = (n: Notifd.Notification) => {
const { LOW, NORMAL, CRITICAL } = Notifd.Urgency
// match operator when?
switch (n.urgency) {
case LOW: return "low"
case CRITICAL: return "critical"
case NORMAL:
default: return "normal"
}
}
type Props = {
delete( id: number, instanceID: number ): void
setup(self: EventBox): void
onHoverLost(self: EventBox): void
notification: Notifd.Notification
id: number
instanceID: number
}
export default function Notification(props: Props) {
const { notification: n, onHoverLost, setup, id: id, delete: del, instanceID: instance } = props
const { START, CENTER, END } = Gtk.Align
return <eventbox
className={`Notification ${urgency(n)}`}
setup={setup}
onHoverLost={onHoverLost}>
<box vertical>
<box className="header">
{(n.appIcon || n.desktopEntry) && <icon
className="app-icon"
visible={Boolean(n.appIcon || n.desktopEntry)}
icon={n.appIcon || n.desktopEntry}
/>}
<label
className="app-name"
halign={START}
truncate
label={n.appName || "Unknown"}
/>
<label
className="time"
hexpand
halign={END}
label={time(n.time)}
/>
<button onClicked={() => del( id, instance )}>
<icon icon="window-close-symbolic" />
</button>
</box>
<Gtk.Separator visible />
<box className="content">
{n.image && fileExists(n.image) && <box
valign={START}
className="image"
css={`background-image: url('${n.image}')`}
/>}
{n.image && isIcon(n.image) && <box
expand={false}
valign={START}
className="icon-image">
<icon icon={n.image} expand halign={CENTER} valign={CENTER} />
</box>}
<box vertical>
<label
className="summary"
halign={START}
xalign={0}
label={n.summary}
truncate
/>
{n.body && <label
className="body"
wrap
useMarkup
halign={START}
xalign={0}
justifyFill
label={n.body}
/>}
</box>
</box>
{n.get_actions().length > 0 && <box className="actions">
{n.get_actions().map(({ label, id }) => (
<button
hexpand
onClicked={() => n.invoke(id)}>
<label label={label} halign={CENTER} hexpand />
</button>
))}
</box>}
</box>
</eventbox>
}
+6
View File
@@ -0,0 +1,6 @@
{
"name": "astal-shell",
"dependencies": {
"astal": "/usr/share/astal/gjs"
}
}
+2
View File
@@ -0,0 +1,2 @@
// Import notification box style
@use "./notifications/notifications.scss"
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"experimentalDecorators": true,
"strict": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
// "checkJs": true,
// "allowJs": true,
"jsx": "react-jsx",
"jsxImportSource": "astal/gtk3",
}
}
+2
View File
@@ -0,0 +1,2 @@
node_modules/
@girs/
+119
View File
@@ -0,0 +1,119 @@
import {
App
} from 'astal/gtk4';
import AstalHyprland from 'gi://AstalHyprland?version=0.1';
import Bar from './components/bar/Bar';
import Brightness from './util/brightness';
import {
hyprToGdk
} from './util/hyprland';
import style from './style.scss';
App.start( {
'instanceName': 'runner',
'css': style,
main () {
const hypr = AstalHyprland.get_default();
const bars = new Map<number, string>();
const barCreator = ( monitor: AstalHyprland.Monitor ) => {
const gdkMonitor = hyprToGdk( monitor );
if ( gdkMonitor ) {
print( 'Bar added for screen ' + monitor.get_id() );
bars.set( monitor.get_id(), Bar.BarLauncher( gdkMonitor ) );
}
};
for ( const monitor of hypr.monitors ) {
barCreator( monitor );
}
hypr.connect( 'monitor-added', ( _, monitor ) => {
barCreator( monitor );
} );
hypr.connect( 'monitor-removed', ( _, monitor ) => {
const windowName = bars.get( monitor );
if ( windowName ) {
const win = App.get_window( windowName );
if ( win ) {
App.toggle_window( windowName );
win.set_child( null );
App.remove_window( win );
print( 'Bar removed for screen', monitor );
}
bars.delete( monitor );
}
} );
// const monitors = App.get_monitors();
// print( "adding bar to monitors" );
// for (let index = 0; index < monitors.length; index++) {
// Bar.BarLauncher( monitors[ index ] );
// }
// Launcher();
},
requestHandler ( request, res ) {
const args = request.trimStart().split( ' ' );
if ( args[ 0 ] === 'notifier' ) {
res( 'Not available here yet, run astal -i notifier ' + args[ 1 ] );
// res( notifications.cliHandler( args ) );
} else if ( args[ 0 ] === 'bar' ) {
res( Bar.cliHandler( args ) );
} else if ( args[ 0 ] === 'brightness' ) {
try {
const brightness = Brightness.get_default();
if ( brightness.screenAvailable ) {
if ( args[ 1 ] === 'increase' ) {
brightness.screen += args.length > 1 ? parseInt( args[ 2 ] ) / 100 : 1;
} else if ( args[ 1 ] === 'decrease' ) {
brightness.screen -= args.length > 1 ? parseInt( args[ 2 ] ) / 100 : 1;
} else if ( args[ 1 ] === 'set' ) {
if ( args.length > 1 ) {
brightness.screen = parseInt( args[ 2 ] ) / 100;
} else {
res( 'Argument <brightness> unspecified' );
return;
}
} else {
res( 'Unknown command ' + args[ 1 ] );
return;
}
res( 'Ok' );
} else {
res( 'No controllable screen available' );
}
} catch ( e ) {
print( e );
res( 'Error running brightness change' );
}
} else if ( args[ 0 ] === 'reload-style' ) {
res( 'Reloading style sheets' );
App.apply_css( '', true );
// TODO: Compile scss and then load it
}
// } else if ( args[ 0 ] === 'launcher' ) {
// if ( args[ 1 ] === 'show' ) {
// App.get_window( 'launcher' )?.show();
// res( '[Launcher] Shown' );
// } else if ( args[ 1 ] === 'hide' ) {
// App.get_window( 'launcher' )?.hide();
// res( '[Launcher] Hidden' );
// } else if ( args[ 1 ] === 'toggle' ) {
// App.toggle_window( 'launcher' );
// res( '[Launcher] Toggled' );
// } else {
// res( '[Launcher] unknown command' );
// }
// }
},
} );
+1
View File
@@ -0,0 +1 @@
false
@@ -0,0 +1,91 @@
import Audio from './modules/Audio/Audio';
import {
BatteryBox
} from './modules/Battery';
import Bluetooth from './modules/Bluetooth/Bluetooth';
import Brightness from './modules/Brightness/Brightness';
import {
Gtk
} from 'astal/gtk4';
import Network from './modules/Networking/Network';
import Player from './modules/Player/Player';
import Power from './modules/Power';
import SysTray from './modules/SysTray';
import {
exec
} from 'astal';
const QuickActions = () => {
const popover = new Gtk.Popover( {
'cssClasses': [ 'quick-actions-wrapper' ]
} );
popover.set_child( renderQuickActions() );
return popover;
};
const renderQuickActions = () => {
const user = exec( '/bin/sh -c whoami' );
const profile = exec( '/bin/fish -c get-profile-picture' );
const cwd = exec( 'pwd' );
const um = Power.UserMenu();
return (
<box visible cssClasses={[
'quick-actions',
'popover-box'
]} vertical>
<centerbox
startWidget={
<button
onClicked={() => um.popup()}
cssClasses={[ 'stealthy-button' ]}
child={
<box>
{um}
<Gtk.Frame
cssClasses={[ 'avatar-icon' ]}
child={
<image
file={
profile !== ''
? profile
: cwd
+ '/no-avatar-icon.jpg'
}
></image>
}
></Gtk.Frame>
<label label={user}></label>
</box>
}
></button>
}
endWidget={
<box
hexpand={false}
>
<BatteryBox></BatteryBox>
<SysTray.SystemTray></SysTray.SystemTray>
<Power.Power></Power.Power>
</box>
}
></centerbox>
<Gtk.Separator marginTop={10} marginBottom={20}></Gtk.Separator>
<box>
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
<Network.Network></Network.Network>
</box>
<Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator>
<Brightness.BrightnessModule></Brightness.BrightnessModule>
<Audio.AudioModule></Audio.AudioModule>
<Player.PlayerModule></Player.PlayerModule>
</box>
);
};
// TODO: Expose additional functions to be usable through CLI
export default {
QuickActions,
};
+35
View File
@@ -0,0 +1,35 @@
import { Gtk } from "astal/gtk4";
import Power from "./modules/Power";
import Audio from "./modules/Audio/Audio";
import Bluetooth from "./modules/Bluetooth/Bluetooth";
import Brightness from "./modules/Brightness/Brightness";
import Player from "./modules/Player/Player";
import { BatteryBox } from "./modules/Battery";
const QuickActions = () => {
const popover = new Gtk.Overlay( { cssClasses: [ 'quick-actions-wrapper' ] } );
popover.set_child(renderQuickActions());
return popover;
};
const renderQuickActions = () => {
return (
<box visible cssClasses={["quick-actions", "popover-box"]} vertical setup={ self }>
<box halign={Gtk.Align.END}>
<BatteryBox></BatteryBox>
<Power></Power>
</box>
<Bluetooth.BluetoothModule></Bluetooth.BluetoothModule>
<Gtk.Separator marginTop={10} marginBottom={10}></Gtk.Separator>
<Brightness.BrightnessModule></Brightness.BrightnessModule>
<Audio.AudioModule></Audio.AudioModule>
<Gtk.Separator marginTop={20} marginBottom={10}></Gtk.Separator>
<Player.PlayerModule></Player.PlayerModule>
</box>
);
};
// TODO: Expose additional functions to be usable through CLI
export default {
QuickActions,
};
@@ -0,0 +1,3 @@
.audio-box {
min-width: 320px;
}
@@ -0,0 +1,191 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import AstalWp from "gi://AstalWp";
const wp = AstalWp.get_default()!;
const AudioModule = () => {
const setVolumeSpeaker = (volume: number) => {
wp.defaultSpeaker.set_volume(volume / 100);
};
const setVolumeMicrophone = (volume: number) => {
wp.defaultMicrophone.set_volume(volume / 100);
};
const speakerSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_SINK);
const micSelector = SinkSelectPopover(AstalWp.MediaClass.AUDIO_SOURCE);
return (
<box cssClasses={["audio-box"]} vertical>
<box hexpand vexpand>
<button
onClicked={() =>
wp.defaultSpeaker.set_mute(
!wp.defaultSpeaker.get_mute(),
)
}
tooltipText={"Mute audio output"}
child={
<image
iconName={bind(wp.defaultSpeaker, "volumeIcon")}
marginEnd={3}
></image>
}
></button>
<label
label={bind(wp.defaultSpeaker, "volume").as(
v => Math.round(100 * v) + "%",
)}
></label>
<slider
value={bind(wp.defaultSpeaker, "volume").as(v => 100 * v)}
max={100}
min={0}
step={1}
hexpand
vexpand
onChangeValue={self => setVolumeSpeaker(self.value)}
></slider>
<button
cssClasses={["sink-select-button"]}
tooltipText={"Pick audio output"}
child={
<box>
<image iconName={"speaker-symbolic"}></image>
{speakerSelector}
</box>
}
onClicked={() => speakerSelector.popup()}
></button>
</box>
<box hexpand vexpand>
<button
onClicked={() =>
wp.defaultMicrophone.set_mute(
!wp.defaultMicrophone.get_mute(),
)
}
tooltipText={"Mute audio input"}
child={
<image
iconName={bind(wp.defaultMicrophone, "volumeIcon")}
marginEnd={3}
></image>
}
></button>
<label
label={bind(wp.defaultMicrophone, "volume").as(
v => Math.round(100 * v) + "%",
)}
></label>
<slider
value={bind(wp.defaultMicrophone, "volume").as(
v => 100 * v,
)}
max={100}
min={0}
step={1}
hexpand
vexpand
onChangeValue={self => setVolumeMicrophone(self.value)}
></slider>
<button
cssClasses={["sink-select-button"]}
tooltipText={"Select audio input"}
child={
<box>
<image iconName={"microphone"}></image>
{micSelector}
</box>
}
onClicked={() => micSelector.popup()}
></button>
</box>
</box>
);
};
const SinkPicker = (type: AstalWp.MediaClass) => {
const devices = bind(wp, "nodes");
wp.connect("ready", () => {
const dev = wp.get_nodes()!
for (let i = 0; i < dev.length; i++) {
const d = dev[i];
}
})
return (
<box vertical>
<label
label={`Available Audio ${type === AstalWp.MediaClass.AUDIO_SINK ? "Output" : type === AstalWp.MediaClass.AUDIO_SOURCE ? "Input" : ""} Devices`}
cssClasses={[ 'title-2' ]}
></label>
<Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator>
<box vertical cssClasses={["sink-picker"]}>
{devices.as(d => {
return d.map(device => {
if (device.get_media_class() !== type) {
return <box cssClasses={[ 'empty' ]}></box>;
}
return (
<button
cssClasses={bind(device, "id").as(id => {
if (
id ===
(type ===
AstalWp.MediaClass.AUDIO_SINK
? wp.defaultSpeaker.id
: type ===
AstalWp.MediaClass
.AUDIO_SOURCE
? wp.defaultMicrophone.id
: "")
) {
return [
"sink-option",
"currently-selected-sink-option",
];
} else {
return ["sink-option"];
}
})}
child={
<box halign={Gtk.Align.START}>
<image
iconName={bind(device, "icon").as(
icon => icon,
)}
marginEnd={3}
></image>
<label
label={bind(
device,
"description",
).as(t => t ?? "")}
></label>
</box>
}
onClicked={() => {
device.set_is_default(true);
}}
></button>
);
});
})}
</box>
</box>
);
};
const SinkSelectPopover = (type: AstalWp.MediaClass) => {
const popover = new Gtk.Popover();
popover.set_child(SinkPicker(type));
return popover;
};
export default {
AudioModule,
};
@@ -0,0 +1,51 @@
{devices.as(d => {
return d.map(device => {
if (device.get_media_class() !== type) {
return <box cssClasses={[ 'empty' ]}></box>;
}
return (
<button
cssClasses={bind(device, "id").as(id => {
if (
id ===
(type ===
AstalWp.MediaClass.AUDIO_SPEAKER
? wp.defaultSpeaker.id
: type ===
AstalWp.MediaClass
.AUDIO_MICROPHONE
? wp.defaultMicrophone.id
: "")
) {
return [
"sink-option",
"currently-selected-sink-option",
];
} else {
return ["sink-option"];
}
})}
child={
<box halign={Gtk.Align.START}>
<image
iconName={bind(device, "icon").as(
icon => icon,
)}
marginEnd={3}
></image>
<label
label={bind(
device,
"description",
).as(t => t ?? "")}
></label>
</box>
}
onClicked={() => {
device.set_is_default(true);
}}
></button>
);
});
})}
@@ -0,0 +1,57 @@
import { bind } from "astal";
import Battery from "gi://AstalBattery";
export const BatteryBox = () => {
const battery = Battery.get_default();
const batteryEnergy = (energyRate: number) => {
return energyRate > 0.1 ? `${Math.round(energyRate * 10) / 10} W ` : "";
};
return (
<box
cssClasses={["battery-info"]}
visible={bind(battery, "isBattery")}
hexpand={false}
vexpand={false}
>
<image
iconName={bind(battery, "batteryIconName")}
tooltipText={bind(battery, "energyRate").as(er =>
batteryEnergy(er),
)}
/>
<label
label={bind(battery, "percentage").as(
p => ` ${Math.round(p * 100)}%`,
)}
marginEnd={3}
/>
<label
cssClasses={["battery-time"]}
visible={bind(battery, "charging").as(c => !c)}
label={bind(battery, "timeToEmpty").as(t => `(${toTime(t)})`)}
tooltipText={bind(battery, 'energyRate').as(er => `Time to empty. Power usage: ${batteryEnergy(er)}`)}
/>
<label
cssClasses={["battery-time"]}
visible={bind(battery, "charging")}
label={bind(battery, "timeToFull").as(t => `(${toTime(t)})`)}
tooltipText={bind(battery, 'energyRate').as(er => `Time to full. Charge rate: ${batteryEnergy(er)}`)}
/>
</box>
);
};
const MINUTE = 60;
const HOUR = MINUTE * 60;
const toTime = (time: number) => {
if (!time) return "Waiting on BIOS"
if (time > 24 * HOUR) return "24h+";
const hours = Math.round(time / HOUR);
const minutes = Math.round((time - hours * HOUR) / MINUTE);
const hoursDisplay = hours > 0 ? `${hours}h` : "";
const minutesDisplay = minutes > 0 ? `${minutes}m` : "";
return `${hoursDisplay}${minutesDisplay}`;
};
@@ -0,0 +1,222 @@
import { bind, interval, readFile, timeout, writeFile } from "astal";
import { Gtk } from "astal/gtk4";
import AstalBluetooth from "gi://AstalBluetooth";
import BTDevice from "./Device";
const ALIGN = Gtk.Align;
const bt = AstalBluetooth.get_default();
const BluetoothModule = () => {
const picker = BluetoothPicker();
const openBTPicker = () => {
try {
bt.adapter.start_discovery();
} catch (e) {
printerr(e);
}
picker.popup();
};
return (
<box>
<button
cssClasses={bind(bt, "isPowered").as(powered =>
powered
? ["toggle-button", "toggle-on"]
: ["toggle-button"],
)}
onClicked={() => {
try {
bt.adapter.set_powered(!bt.adapter.get_powered())
} catch (_) { }
}}
child={
<box vertical>
<label
cssClasses={["title-2"]}
label={"Bluetooth"}
halign={ALIGN.CENTER}
valign={ALIGN.CENTER}
></label>
<box halign={ALIGN.CENTER} valign={ALIGN.CENTER}>
<label
visible={bind(bt, "isPowered").as(
p => !p,
)}
label="Disabled"
></label>
<label
visible={bind(bt, "isPowered")}
label={bind(bt, "devices").as(devices => {
let count = 0;
devices.forEach(device => {
if (device.connected) {
count++;
}
});
return `On (${count} ${count === 1 ? "client" : "clients"} connected)`;
})}
></label>
</box>
<label></label>
</box>
}
></button>
<button
cssClasses={["actions-button"]}
visible={bind(bt, "isPowered")}
child={
<box>
<image iconName={"arrow-right-symbolic"}></image>
{picker}
</box>
}
tooltipText={"View available devices"}
onClicked={() => openBTPicker()}
></button>
</box>
);
};
const BluetoothPickerList = () => {
let btEnableState = false;
try {
btEnableState = readFile("./btconf") === "true" ? true : false;
} catch (_) { }
if (bt.get_adapter()) {
print('Setting BT state to ' + btEnableState);
bt.adapter.set_powered(btEnableState);
} else {
timeout(5000, () => {
if (bt.get_adapter()) {
print('Setting BT state to ' + btEnableState);
bt.adapter.set_powered(btEnableState);
} else {
timeout(5000, () => {
try {
bt.adapter.set_powered(btEnableState);
} catch (_) { }
})
}
})
}
const updateState = () => {
btEnableState = !btEnableState;
writeFile("./btconf", "" + btEnableState);
};
return (
<box
vertical
onDestroy={() => {
try {
bt.adapter.stop_discovery()
} catch (_) { }
}}
cssClasses={["popover-box"]}
>
<label cssClasses={["title"]} label={"Bluetooth"}></label>
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
<centerbox
startWidget={<label label={"Turn on at startup"}></label>}
endWidget={
<switch
valign={ALIGN.END}
halign={ALIGN.END}
active={btEnableState}
onButtonPressed={() => updateState()}
></switch>
}
></centerbox>
<label
marginTop={10}
label={"Connected & Trusted devices"}
cssClasses={["title-2"]}
></label>
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
<box vertical cssClasses={["devices-list"]}>
{bind(bt, "devices").as(devices => {
return devices
.filter(device => {
if (device.get_connected() || device.get_paired()) {
return device;
}
})
.map(device => {
return <BTDevice device={device}></BTDevice>;
});
})}
</box>
<label
visible={bind(bt, "devices").as(devices => {
return (
devices.filter(device => {
if (device.get_connected() || device.get_paired()) {
return device;
}
}).length === 0
);
})}
label={"No connected / trusted devices"}
cssClasses={["bt-no-found", "bt-conn-list"]}
></label>
<label
label={"Discovered bluetooth devices"}
cssClasses={["title-2"]}
></label>
<Gtk.Separator marginBottom={5} marginTop={3}></Gtk.Separator>
<box vertical>
{bind(bt, "devices").as(devices => {
return devices
.filter(data => {
if (!data.get_connected() && !data.get_paired()) {
return data;
}
})
.map(device => {
return <BTDevice device={device}></BTDevice>;
});
})}
</box>
<label
visible={bind(bt, "devices").as(devices => {
return (
devices.filter(device => {
if (
!device.get_connected() &&
!device.get_paired()
) {
return device;
}
}).length === 0
);
})}
label={"No discovered devices"}
cssClasses={["bt-no-found"]}
></label>
</box>
);
};
const BluetoothPicker = () => {
const popover = new Gtk.Popover();
popover.set_child(BluetoothPickerList());
popover.connect("closed", () => {
try {
bt.adapter.stop_discovery();
} catch (e) {
printerr(e);
}
});
return popover;
};
export default {
BluetoothModule,
};
@@ -0,0 +1,72 @@
import { bind } from "astal";
import AstalBluetooth from "gi://AstalBluetooth";
const BTDevice = ({ device }: { device: AstalBluetooth.Device }) => {
return (
<button
visible={bind(device, "name").as(n => n !== null)}
child={
<centerbox
startWidget={
<box>
<image
iconName={"chronometer-reset"}
tooltipText={"Device is currently connecting"}
visible={bind(device, "connecting")}
></image>
<image
iconName={bind(device, "icon")}
marginEnd={3}
></image>
</box>
}
centerWidget={
<label
label={bind(device, "name").as(n => n ?? "No name")}
marginEnd={5}
></label>
}
endWidget={
<box>
<label
label={bind(device, "batteryPercentage").as(
bat => (bat >= 0 ? bat + "%" : "?%"),
)}
tooltipText={"Device's battery percentage"}
marginEnd={3}
></label>
<image
iconName={bind(device, "paired").as(v =>
v ? "network-bluetooth-activated-symbolic" : "bluetooth-disconnected-symbolic",
)}
></image>
<button tooltipText={"Device trusted status"} child={
<image
iconName={bind(device, "trusted").as(v =>
v ? "checkbox" : "window-close-symbolic",
)}
></image>
} onClicked={() => device.set_trusted( !device.get_trusted() )}
cssClasses={[ 'button-no-margin' ]}
></button>
</box>
}
></centerbox>
}
onClicked={() => {
connectOrPair( device );
}}
></button>
);
};
const connectOrPair = (device: AstalBluetooth.Device) => {
if ( device.get_paired() ) {
device.connect_device(() => { });
// Show failed message if tried to connect and failed
} else {
device.pair();
}
};
export default BTDevice;
@@ -0,0 +1,30 @@
import { bind } from "astal";
import Brightness from "../../../../util/brightness";
const brightness = Brightness.get_default();
const BrightnessModule = () => {
const setBrightness = (value: number) => {
brightness.screen = value;
}
return (
<box visible={bind(brightness, 'screenAvailable')}>
<image iconName={"brightness-high-symbolic"}></image>
<label label={bind(brightness, "screen").as(b => `${Math.round(100 * b)}%`)}></label>
<slider
value={Math.round( brightness.screen * 100) / 100}
hexpand
max={1}
min={0.01}
step={0.05}
vexpand
onChangeValue={self => setBrightness(self.value)}
></slider>
</box>
);
};
export default {
BrightnessModule
};
@@ -0,0 +1,221 @@
import { execAsync, bind } from "astal";
import Network from "gi://AstalNetwork";
import { App, Gtk } from "astal/gtk4";
import { NetworkItem } from "./modules/NetworkItem";
import { PasswordDialog } from "./modules/PasswordDialog";
import {
availableNetworks,
savedNetworks,
activeNetwork,
showPasswordDialog,
scanNetworks,
getSavedNetworks,
disconnectNetwork,
forgetNetwork,
isExpanded,
refreshIntervalId,
} from "./networkinghelper";
// Main WiFi Box component
export const WiFiBox = () => {
const network = Network.get_default();
// Initial scan when component is first used
setTimeout(() => {
scanNetworks();
getSavedNetworks();
}, 100);
return (
<box vertical cssClasses={["wifi-menu", "toggle"]}>
{/* WiFi Toggle Header */}
<box cssClasses={["toggle", "wifi-toggle"]}>
<button
onClicked={() => {
if (network.wifi.enabled) {
network.wifi.set_enabled(false);
} else network.wifi.set_enabled(true);
}}
cssClasses={bind(network.wifi, "enabled").as((enabled) =>
enabled ? ["button"] : ["button-disabled"],
)}
>
<image iconName={bind(network.wifi, "icon_name")} />
</button>
<button
hexpand={true}
onClicked={() => {
if (network.wifi.enabled) {
isExpanded.set(!isExpanded.get());
if (isExpanded.get()) {
scanNetworks();
getSavedNetworks();
}
}
}}
>
<box hexpand={true}>
<label
hexpand={true}
xalign={0}
label={bind(network.wifi, "ssid").as(
(ssid) =>
ssid || (network.wifi.enabled ? "Not Connected" : "WiFi Off"),
)}
/>
<image
iconName="pan-end-symbolic"
halign={Gtk.Align.END}
cssClasses={bind(isExpanded).as((expanded) =>
expanded
? ["arrow-indicator", "arrow-down"]
: ["arrow-indicator"],
)}
/>
</box>
</button>
</box>
{/* Networks List Revealer */}
<revealer
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
transitionDuration={300}
revealChild={bind(isExpanded)}
setup={() => {
const clearScanInterval = () => {
if (refreshIntervalId.get()) {
clearInterval( parseInt( '' + refreshIntervalId.get() ));
refreshIntervalId.set(null);
}
};
bind(isExpanded).subscribe((expanded) => {
// Clear existing interval
clearScanInterval();
if (expanded) {
// Scan networks
network.wifi?.scan();
// Set up new interval if WiFi is enabled
if (network.wifi?.enabled) {
refreshIntervalId.set(
setInterval(() => {
scanNetworks();
getSavedNetworks();
print("updated");
}, 10000),
);
}
} else {
// Apply revealer bug fix when collapsed
App.toggle_window("system-menu");
App.toggle_window("system-menu");
}
});
// Monitor window toggling
const windowListener = App.connect("window-toggled", (_, window) => {
if (window.name === "system-menu" && isExpanded.get()) {
isExpanded.set(false);
}
});
// Clean up resources when component is destroyed
return () => {
App.disconnect(windowListener);
clearScanInterval();
};
}}
>
<box vertical cssClasses={["network-list"]}>
<box visible={showPasswordDialog( v => v )}>
<PasswordDialog />
</box>
<label label="Available Networks" cssClasses={["section-label"]} />
<label label="No networks found" cssClasses={["empty-label"]} visible={availableNetworks( net => net.length === 0 )}/>
<box visible={availableNetworks( networks => networks.length > 1 )}>
{availableNetworks( networks =>
networks.map( (network) => <NetworkItem network={network} />)
)}
</box>
{savedNetworks((networks) => {
// Filter out networks already shown in available networks
const filteredNetworks = networks.filter(
(ssid) => !availableNetworks.get().some((n) => n.ssid === ssid)
);
// Only render the section if there are filtered networks to show
return filteredNetworks.length > 0 ? (
<box vertical>
<label label="Saved Networks" cssClasses={["section-label"]} />
{filteredNetworks.map((ssid) => (
<box cssClasses={["saved-network"]}>
<label label={ssid} />
<box hexpand={true} />
<button
label="Forget"
cssClasses={["forget-button", "button"]}
onClicked={() => forgetNetwork(ssid)}
/>
</box>
))}
</box>
) : (
<box></box>
);
})}
<box hexpand>
<button
halign={Gtk.Align.START}
cssClasses={["refresh-button"]}
onClicked={() => {
scanNetworks();
getSavedNetworks();
}}
>
<image iconName="view-refresh-symbolic" />
</button>
{/* Connected Network Options */}
<box hexpand>
{activeNetwork((active) =>
active ? (
<box vertical cssClasses={["connected-network"]} hexpand>
<button
label="Disconnect"
cssClasses={["disconnect-button"]}
onClicked={() => disconnectNetwork(active.ssid)}
/>
</box>
) : (
""
),
)}
</box>
<button
cssClasses={["settings-button"]}
halign={Gtk.Align.END}
hexpand={false}
onClicked={() => {
execAsync([
"sh",
"-c",
"XDG_CURRENT_DESKTOP=GNOME gnome-control-center wifi",
]);
isExpanded.set(false);
}}
>
<image iconName={"emblem-system-symbolic"} />
</button>
) : (
""
),
)}
</box>
</box>
</revealer>
</box>
);
};
@@ -0,0 +1,2 @@
# Source
This is a modified version from [MatShell](https://github.com/Neurian/matshell)
@@ -0,0 +1,14 @@
import AstalNetwork from "gi://AstalNetwork?version=0.1";
interface CurrentWiFi {
ssid: string;
strength: number;
secured: boolean;
}
interface WiFiDetails extends CurrentWiFi {
active: boolean;
accessPoint: AstalNetwork.AccessPoint;
iconName: string;
}
@@ -0,0 +1,161 @@
// From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts
import { execAsync, Variable } from "astal";
import Network from "gi://AstalNetwork";
import { CurrentWiFi, WiFiDetails } from "./network";
// State trackers
export const availableNetworks: Variable<WiFiDetails[]> = Variable([]);
export const savedNetworks: Variable<string[]> = Variable([]);
export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null);
export const isConnecting: Variable<boolean> = Variable(false);
export const showPasswordDialog: Variable<boolean> = Variable(false);
export const errorMessage: Variable<string> = Variable("");
export const isExpanded: Variable<boolean> = Variable(false);
export const passwordInput: Variable<string> = Variable("");
export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null);
export const refreshIntervalId: Variable<
number | null | ReturnType<typeof setTimeout>
> = Variable(null);
// Function to scan for available networks
export const scanNetworks = () => {
const network = Network.get_default();
if (network && network.wifi) {
network.wifi.scan();
// Get available networks from access points
const networks: WiFiDetails[] = network.wifi.accessPoints
.map(ap => ({
ssid: ap.ssid,
strength: ap.strength,
secured: ap.flags !== 0,
active: network.wifi.activeAccessPoint?.ssid === ap.ssid,
accessPoint: ap,
iconName: ap.iconName,
}))
.filter(n => n.ssid);
// Sort by signal strength
networks.sort((a, b) => b.strength - a.strength);
// Remove duplicates (same SSID)
const uniqueNetworks: WiFiDetails[] = [];
const seen = new Set();
networks.forEach(network => {
if (!seen.has(network.ssid)) {
seen.add(network.ssid);
uniqueNetworks.push(network);
}
});
availableNetworks.set(uniqueNetworks);
// Update active network
if (network.wifi.activeAccessPoint) {
activeNetwork.set({
ssid: network.wifi.activeAccessPoint.ssid,
strength: network.wifi.activeAccessPoint.strength,
secured: network.wifi.activeAccessPoint.flags !== 0,
});
} else {
activeNetwork.set(null);
}
}
};
// Function to list saved networks
export const getSavedNetworks = () => {
execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"])
.then(output => {
if (typeof output === "string") {
const savedWifiNetworks = output
.split("\n")
.filter(line => line.includes("802-11-wireless"))
.map(line => line.split(":")[0].trim());
savedNetworks.set(savedWifiNetworks);
}
})
.catch(error => console.error("Error fetching saved networks:", error));
};
// Function to connect to a network
export const connectToNetwork = (
ssid: string,
password: null | string = null,
) => {
isConnecting.set(true);
errorMessage.set("");
const network = Network.get_default();
const currentSsid = network.wifi.ssid;
// Function to perform the actual connection
const performConnection = () => {
let command = "";
if (password) {
// Connect with password
command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`;
} else {
// Connect without password (saved or open network)
command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`;
}
execAsync(["bash", "-c", command])
.then(() => {
showPasswordDialog.set(false);
isConnecting.set(false);
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Connection error:", error);
errorMessage.set("Failed to connect. Check password.");
isConnecting.set(false);
});
};
// If already connected to a network, disconnect first
if (currentSsid && currentSsid !== ssid) {
console.log(
`Disconnecting from ${currentSsid} before connecting to ${ssid}`,
);
execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`])
.then(() => {
// Wait a moment for the disconnection to complete fully
setTimeout(() => {
performConnection();
}, 500); // 500ms delay for clean disconnection
})
.catch(error => {
console.error("Disconnect error:", error);
// Continue with connection attempt even if disconnect fails
performConnection();
});
} else {
// No active connection or connecting to same network (reconnect case)
performConnection();
}
};
// Function to disconnect from a network
export const disconnectNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection down "${ssid}"`])
.then(() => {
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Disconnect error:", error);
});
};
// Function to forget a saved network
export const forgetNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`])
.then(() => {
getSavedNetworks(); // Refresh saved networks list
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Forget network error:", error);
});
};
@@ -0,0 +1,106 @@
import { bind } from "astal";
import AstalNetwork from "gi://AstalNetwork";
import networkHelper from "./network-helper";
import NetworkMenu from "./NetworkMenu";
const net = AstalNetwork.get_default();
const STATE = AstalNetwork.DeviceState;
const Network = () => {
const netMenu = NetworkMenu.NetworkMenu();
return (
<box>
<button
cssClasses={networkHelper.networkEnabled(en => {
if (en) return ["toggle-button", "toggle-on"];
else return ["toggle-button"];
})}
onClicked={() =>
networkHelper.setNetworking(
!networkHelper.networkEnabled.get(),
)
}
child={
<box vertical>
<label
label={bind(net.wifi, "enabled").as(
stat => `Network (${stat ? "WiFi" : "Wired"})`,
)}
cssClasses={["title-2"]}
></label>
<box child=
{bind(net, 'wired').as(v => {
if (v) {
return <label
label={bind(net.wired, "state").as(state => {
if (state === STATE.ACTIVATED) {
return (
"Wired. IP: " + networkHelper.getIP()
);
} else if (state === STATE.DISCONNECTED) {
return "Disconnected";
} else if (state === STATE.FAILED) {
return "Error";
} else if (
state === STATE.PREPARE ||
state === STATE.CONFIG ||
state === STATE.IP_CHECK ||
state === STATE.IP_CONFIG
) {
return "Connecting...";
} else {
return "Unavailable";
}
})}
visible={bind(net.wifi, "enabled").as(v => !v)}
></label>
} else {
return <label
label={"State unavailable"}
visible={bind(net.wifi, "enabled").as(v => !v)}
></label>
}
})}></box>
<label
label={bind(net.wifi, "state").as(state => {
if (state === STATE.ACTIVATED) {
return `${net.wifi.get_ssid()} (${networkHelper.getIP()})`;
} else if (state === STATE.DISCONNECTED) {
return "Disconnected";
} else if (state === STATE.FAILED) {
return "Error";
} else if (
state === STATE.PREPARE ||
state === STATE.CONFIG ||
state === STATE.IP_CHECK ||
state === STATE.IP_CONFIG
) {
return "Connecting...";
} else {
return "Unavailable";
}
})}
visible={bind(net.wifi, "enabled")}
></label>
</box>
}
></button >
<button
cssClasses={["actions-button"]}
visible={networkHelper.networkEnabled()}
onClicked={() => netMenu.popup()}
child={
<box>
<image iconName={"arrow-right-symbolic"}></image>
{netMenu}
</box>
}
tooltipText={"View available devices"}
></button>
</box >
);
};
export default {
Network,
};
@@ -0,0 +1,18 @@
import { Gtk } from "astal/gtk4";
const NetworkMenu = () => {
const popover = new Gtk.Popover();
popover.set_child( renderMenu() );
return popover;
};
const renderMenu = () => {
return <box vertical>
<image iconName={"appointment-soon-symbolic"} iconSize={Gtk.IconSize.LARGE}></image>
<label label={"Coming later"}></label>
</box>;
};
export default {
NetworkMenu,
};
@@ -0,0 +1,91 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import AstalNetwork from "gi://AstalNetwork";
import networkHelper from "./network-helper";
const net = AstalNetwork.get_default();
const STATE = AstalNetwork.DeviceState;
const WiFiList = () => {
const popover = new Gtk.Popover({ cssClasses: ["WiFiPicker"] });
return popover;
};
const renderWiFiList = () => {
return <box>
<label label="Test"></label>
</box>
}
const Network = () => {
const wifiList = WiFiList();
return (
<box>
<button
cssClasses={networkHelper.networkEnabled(en => {
if (en) return ["network-button", "net-on"];
else return ["network-button"];
})}
onClicked={() =>
networkHelper.setNetworking(
!networkHelper.networkEnabled.get(),
)
}
child={<box vertical>
<label label="Wired" cssClasses={[ 'button-name' ]}></label>
<label label={bind( net.wired, 'state' ).as( state => {
if ( state === STATE.ACTIVATED ) {
return 'Connected. IP: ' + networkHelper.getIP();
} else if ( state === STATE.DISCONNECTED ) {
return 'Disconnected';
} else if ( state === STATE.FAILED ) {
return 'Error';
} else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) {
return 'Connecting...';
} else {
return 'Unavailable';
}
} )}></label>
</box>}
></button>
<box>
<button
cssClasses={bind(net.wifi, "enabled").as(b => {
const classes = ["network-button"];
if (b) {
classes.push("wifi-on");
}
return classes;
})}
child={<box vertical>
<label label="WiFi" cssClasses={[ 'button-name' ]}></label>
<label label={bind( net.wifi, 'state' ).as( state => {
if ( state === STATE.ACTIVATED ) {
return 'Connected. IP: ' + networkHelper.getIP();
} else if ( state === STATE.DISCONNECTED ) {
return 'Disconnected';
} else if ( state === STATE.FAILED ) {
return 'Error';
} else if ( state === STATE.PREPARE || state === STATE.CONFIG || state === STATE.IP_CHECK || state === STATE.IP_CONFIG ) {
return 'Connecting...';
} else {
return 'Unavailable';
}
} )} visible={bind(net.wifi, 'enabled').as( en => en )}></label>
<label label="Disabled" visible={bind(net.wifi, 'enabled').as( en => !en )}></label>
</box>}
></button>
<button
cssClasses={["network-button-context"]}
visible={bind(net.wifi, "enabled").as(b => b)}
onClicked={() => wifiList.popup()}
></button>
</box>
{wifiList}
</box>
);
};
export default Network;
@@ -0,0 +1,28 @@
import { exec, Variable } from "astal";
import AstalNetwork from "gi://AstalNetwork";
const networkEnabled = Variable( exec( 'nmcli networking connectivity' ) !== 'none' );
const network = AstalNetwork.get_default();
const setNetworking = ( status: boolean ) => {
if ( status === true ) {
exec( 'nmcli networking on' );
networkEnabled.set( true );
} else {
exec( 'nmcli networking off' );
networkEnabled.set( false );
}
}
const getIP = () => {
return exec( `/bin/bash -c "ip addr show | grep 'inet ' | awk '{print $2}' | grep -v '127'"` ).split( '/' )[ 0 ];
}
export default {
networkEnabled,
setNetworking,
getIP
}
@@ -0,0 +1,14 @@
import AstalNetwork from "gi://AstalNetwork?version=0.1";
interface CurrentWiFi {
ssid: string;
strength: number;
secured: boolean;
}
interface WiFiDetails extends CurrentWiFi {
active: boolean;
accessPoint: AstalNetwork.AccessPoint;
iconName: string;
}
@@ -0,0 +1,161 @@
// From https://github.com/Neurarian/matshell/blob/master/utils/wifi.ts
import { execAsync, Variable } from "astal";
import Network from "gi://AstalNetwork";
import { CurrentWiFi, WiFiDetails } from "./network";
// State trackers
export const availableNetworks: Variable<WiFiDetails[]> = Variable([]);
export const savedNetworks: Variable<string[]> = Variable([]);
export const activeNetwork: Variable<CurrentWiFi | null> = Variable(null);
export const isConnecting: Variable<boolean> = Variable(false);
export const showPasswordDialog: Variable<boolean> = Variable(false);
export const errorMessage: Variable<string> = Variable("");
export const isExpanded: Variable<boolean> = Variable(false);
export const passwordInput: Variable<string> = Variable("");
export const selectedNetwork: Variable<null | WiFiDetails> = Variable(null);
export const refreshIntervalId: Variable<
number | null | ReturnType<typeof setTimeout>
> = Variable(null);
// Function to scan for available networks
export const scanNetworks = () => {
const network = Network.get_default();
if (network && network.wifi) {
network.wifi.scan();
// Get available networks from access points
const networks: WiFiDetails[] = network.wifi.accessPoints
.map(ap => ({
ssid: ap.ssid,
strength: ap.strength,
secured: ap.flags !== 0,
active: network.wifi.activeAccessPoint?.ssid === ap.ssid,
accessPoint: ap,
iconName: ap.iconName,
}))
.filter(n => n.ssid);
// Sort by signal strength
networks.sort((a, b) => b.strength - a.strength);
// Remove duplicates (same SSID)
const uniqueNetworks: WiFiDetails[] = [];
const seen = new Set();
networks.forEach(network => {
if (!seen.has(network.ssid)) {
seen.add(network.ssid);
uniqueNetworks.push(network);
}
});
availableNetworks.set(uniqueNetworks);
// Update active network
if (network.wifi.activeAccessPoint) {
activeNetwork.set({
ssid: network.wifi.activeAccessPoint.ssid,
strength: network.wifi.activeAccessPoint.strength,
secured: network.wifi.activeAccessPoint.flags !== 0,
});
} else {
activeNetwork.set(null);
}
}
};
// Function to list saved networks
export const getSavedNetworks = () => {
execAsync(["bash", "-c", "nmcli -t -f NAME,TYPE connection show"])
.then(output => {
if (typeof output === "string") {
const savedWifiNetworks = output
.split("\n")
.filter(line => line.includes("802-11-wireless"))
.map(line => line.split(":")[0].trim());
savedNetworks.set(savedWifiNetworks);
}
})
.catch(error => console.error("Error fetching saved networks:", error));
};
// Function to connect to a network
export const connectToNetwork = (
ssid: string,
password: null | string = null,
) => {
isConnecting.set(true);
errorMessage.set("");
const network = Network.get_default();
const currentSsid = network.wifi.ssid;
// Function to perform the actual connection
const performConnection = () => {
let command = "";
if (password) {
// Connect with password
command = `echo '${password}' | nmcli device wifi connect "${ssid}" --ask`;
} else {
// Connect without password (saved or open network)
command = `nmcli connection up "${ssid}" || nmcli device wifi connect "${ssid}"`;
}
execAsync(["bash", "-c", command])
.then(() => {
showPasswordDialog.set(false);
isConnecting.set(false);
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Connection error:", error);
errorMessage.set("Failed to connect. Check password.");
isConnecting.set(false);
});
};
// If already connected to a network, disconnect first
if (currentSsid && currentSsid !== ssid) {
console.log(
`Disconnecting from ${currentSsid} before connecting to ${ssid}`,
);
execAsync(["bash", "-c", `nmcli connection down "${currentSsid}"`])
.then(() => {
// Wait a moment for the disconnection to complete fully
setTimeout(() => {
performConnection();
}, 500); // 500ms delay for clean disconnection
})
.catch(error => {
console.error("Disconnect error:", error);
// Continue with connection attempt even if disconnect fails
performConnection();
});
} else {
// No active connection or connecting to same network (reconnect case)
performConnection();
}
};
// Function to disconnect from a network
export const disconnectNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection down "${ssid}"`])
.then(() => {
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Disconnect error:", error);
});
};
// Function to forget a saved network
export const forgetNetwork = (ssid: string) => {
execAsync(["bash", "-c", `nmcli connection delete "${ssid}"`])
.then(() => {
getSavedNetworks(); // Refresh saved networks list
scanNetworks(); // Refresh network list
})
.catch(error => {
console.error("Forget network error:", error);
});
};
@@ -0,0 +1,56 @@
$fg-color: #{"@theme_fg_color"};
$bg-color: #{"@theme_bg_color"};
box.players-box {
margin-top: 20px;
}
box.player {
padding: 0.6rem;
.cover-art {
min-width: 100px;
min-height: 100px;
border-radius: 9px;
margin-right: 0.6rem;
background-size: contain;
background-position: center;
}
.title {
font-weight: bold;
font-size: 1.1em;
}
scale {
padding: 0;
margin: 0.4rem 0;
border-radius: 20px;
trough {
min-height: 8px;
border-radius: 20px;
}
highlight {
background-color: $fg-color;
border-radius: 20px;
}
slider {
all: unset;
border-radius: 20px;
}
}
centerbox.actions {
min-width: 220px;
button {
min-width: 0;
min-height: 0;
padding: 0.4rem;
margin: 0 0.2rem;
}
}
}
@@ -0,0 +1,155 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import AstalMpris from "gi://AstalMpris";
import Pango from "gi://Pango?version=1.0";
const ALIGN = Gtk.Align;
const mpris = AstalMpris.get_default();
mpris.connect("player-added", p => {
print("Player added:", p);
});
const PlayerModule = () => {
return (
<box vertical cssClasses={ [ 'players-box' ] }>
<label label={"Music Players"} halign={ALIGN.CENTER} cssClasses={[ 'title-2' ]}></label>
<Gtk.Separator marginTop={3} marginBottom={5}></Gtk.Separator>
<box cssClasses={["players"]}>
{bind(mpris, "players").as(players => {
return players.map(player => {
return <PlayerItem player={player}></PlayerItem>;
});
})}
</box>
<label label={"No playback active"} visible={bind(mpris, "players").as( players => players.length === 0 )}></label>
</box>
);
};
// TODO: Update widths
const pbStatus = AstalMpris.PlaybackStatus;
const PlayerItem = ({ player }: { player: AstalMpris.Player }) => {
return (
<box cssClasses={["player"]}>
<image
cssClasses={["cover-art"]}
file={bind(player, "coverArt")}
hexpand
vexpand
></image>
<box vertical>
<label
label={bind(player, "title").as(
title => title ?? "Unknown title",
)}
cssClasses={["title"]}
halign={ALIGN.START}
valign={ALIGN.START}
maxWidthChars={30}
ellipsize={Pango.EllipsizeMode.END}
></label>
<label
label={bind(player, "artist").as(
artist => artist ?? "Unknown artist",
)}
halign={ALIGN.START}
valign={ALIGN.START}
maxWidthChars={30}
ellipsize={Pango.EllipsizeMode.END}
></label>
<slider
visible={bind(player, "length").as(l => l > 0)}
value={bind(player, "position")}
min={0}
max={bind(player, "length")}
onChangeValue={v =>
player.set_position(v.get_value())
}
></slider>
<centerbox
cssClasses={["actions"]}
startWidget={
<label
label={bind(player, "position").as(v =>
secondsToFriendlyTime(v),
)}
hexpand
cssClasses={["position"]}
></label>
}
centerWidget={
<box>
<button
visible={bind(player, "canGoPrevious")}
child={
<image
iconName={
"media-skip-backward-symbolic"
}
></image>
}
onClicked={() => player.previous()}
></button>
<button
visible={bind(player, "canControl")}
child={
<image
iconName={bind(
player,
"playbackStatus",
).as(status => {
if (status === pbStatus.PLAYING) {
return "media-playback-pause-symbolic";
} else {
return "media-playback-start-symbolic";
}
})}
></image>
}
onClicked={() => player.play_pause()}
></button>
<button
visible={bind(player, "canGoNext")}
child={
<image
iconName={"media-skip-forward-symbolic"}
></image>
}
onClicked={() => player.next()}
></button>
</box>
}
endWidget={
<label
cssClasses={["length"]}
hexpand
label={bind(player, "length").as(v =>
secondsToFriendlyTime(v),
)}
></label>
}
></centerbox>
</box>
</box>
);
};
const secondsToFriendlyTime = (time: number) => {
const m = Math.floor(time / 60);
const minutes = Math.floor(m % 60);
const hours = Math.floor(m / 60 % 24);
const seconds = Math.floor(time % 60);
if (hours > 0) {
return `${hours}:${expandTime(minutes)}:${expandTime(seconds)}`;
} else {
return `${minutes}:${expandTime(seconds)}`;
}
};
const expandTime = (time: number): string => {
return time < 10 ? `0${time}` : "" + time;
};
export default {
PlayerModule,
};
@@ -0,0 +1,93 @@
import {
exec
} from 'astal';
import {
Gtk
} from 'astal/gtk4';
const PowerMenu = (): Gtk.Popover => {
const popover = new Gtk.Popover( {
'cssClasses': [ 'PowerMenu' ]
} );
const powerMenuBox = () => {
return (
<box>
<button
cssClasses={[ 'power-button' ]}
child={
<image iconName={'system-shutdown-symbolic'}></image>
}
onClicked={() => exec( '/bin/sh -c \'shutdown now\'' )}
></button>
<button
cssClasses={[ 'power-button' ]}
child={<image iconName={'system-reboot-symbolic'}></image>}
onClicked={() => exec( '/bin/sh -c \'reboot\'' )}
></button>
<button
cssClasses={[ 'power-button' ]}
child={<image iconName={'system-suspend-symbolic'}></image>}
onClicked={() => exec( '/bin/sh -c \'systemctl suspend\'' )}
></button>
</box>
);
};
popover.set_child( powerMenuBox() );
return popover;
};
const Power = () => {
const pm = PowerMenu();
return (
<button
widthRequest={0}
hexpand={false}
vexpand={false}
cssClasses={[ 'power-menu-button' ]}
child={
<box>
<image iconName={'system-shutdown-symbolic'}></image>
{pm}
</box>
}
onClicked={() => pm.popup()}
/>
);
};
const UserMenu = (): Gtk.Popover => {
const popover = new Gtk.Popover();
const powerMenuBox = () => {
return (
<box>
<button
cssClasses={[ 'power-button' ]}
child={
<image iconName={'system-lock-screen-symbolic'}></image>
}
onClicked={() => exec( '/bin/sh -c \'hyprlock\'' )}
></button>
<button
cssClasses={[ 'power-button' ]}
child={<image iconName={'system-log-out-symbolic'}></image>}
onClicked={() => exec( '/bin/sh -c \'hyprctl dispatch exit 0\'' )
}
></button>
</box>
);
};
popover.set_child( powerMenuBox() );
return popover;
};
export default {
Power,
UserMenu
};
@@ -0,0 +1,88 @@
import AstalTray from 'gi://AstalTray';
import {
GObject
} from 'astal';
import {
Gtk
} from 'astal/gtk4';
const SYNC = GObject.BindingFlags.SYNC_CREATE;
const SysTray = () => {
const trayBox = new Gtk.Box( {
'cssClasses': [ '' ]
} );
const tray = AstalTray.get_default();
const trayItems = new Map<string, Gtk.MenuButton>();
const trayAddedHandler = tray.connect( 'item-added', ( _, id ) => {
const item = tray.get_item( id );
const popover = Gtk.PopoverMenu.new_from_model( item.menu_model );
const icon = new Gtk.Image();
const button = new Gtk.MenuButton( {
popover,
'child': icon,
'cssClasses': [ 'tray-item' ],
} );
item.bind_property(
'gicon', icon, 'gicon', SYNC
);
popover.insert_action_group( 'dbusmenu', item.action_group );
item.connect( 'notify::action-group', () => {
popover.insert_action_group( 'dbusmenu', item.action_group );
} );
trayItems.set( id, button );
trayBox.append( button );
} );
const trayRemovedHandler = tray.connect( 'item-removed', ( _, id ) => {
const button = trayItems.get( id );
if ( button ) {
trayBox.remove( button );
button.run_dispose();
trayItems.delete( id );
}
} );
trayBox.connect( 'destroy', () => {
tray.disconnect( trayAddedHandler );
tray.disconnect( trayRemovedHandler );
} );
return trayBox;
};
const TrayPopover = () => {
const popover = new Gtk.Popover( {
'cssClasses': [ 'TrayPopover' ]
} );
popover.set_child( SysTray() );
return popover;
};
const SystemTray = () => {
const systray = TrayPopover();
return (
<button
widthRequest={0}
hexpand={false}
vexpand={false}
cssClasses={[ 'power-menu-button' ]}
child={
<box>
<image iconName={'systemtray'}></image>
{systray}
</box>
}
onClicked={() => systray.popup()}
/>
);
};
export default {
SystemTray
};
@@ -0,0 +1,56 @@
@use "./modules/Player/Player.scss";
@use "./modules/Audio/Audio.scss";
@use "../../util/colours.scss" as *;
.quick-actions-wrapper {
min-width: 520px;
}
box.quick-actions {
padding: 10px;
}
popover * {
border-radius: 20px;
}
button {
margin: 4px;
}
.button-no-margin {
margin-top: 0;
margin-bottom: 0;
}
.devices-list {
margin-bottom: 20px;
}
button.toggle-button {
min-width: 220px;
border-radius: 50px;
&.toggle-on {
min-width: 190px;
margin-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
background-color: $accent-color;
}
}
button.actions-button {
margin-left: 0;
border-radius: 0;
background-color: $accent-color;
border-top-right-radius: 50px;
border-bottom-right-radius: 50px;
}
.avatar-icon {
border-radius: 100px;
min-height: 40px;
min-width: 40px;
margin-right: 10px;
}
+84
View File
@@ -0,0 +1,84 @@
import {
App, Astal, Gdk, Gtk
} from 'astal/gtk4';
import Calendar from './modules/Calendar';
import {
CenterBox
} from 'astal/gtk4/widget';
import Hyprland from './modules/Hyprland';
import QuickView from './modules/QuickView';
import SystemInfo from './modules/SystemInfo';
const Bar = ( {
gdkmonitor, name
}: {
'gdkmonitor': Gdk.Monitor,
'name': string
} ) => {
const {
TOP, LEFT, RIGHT
} = Astal.WindowAnchor;
return (
<window
gdkmonitor={gdkmonitor}
cssClasses={[ 'Bar' ]}
name={name}
namespace={'bar'}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
anchor={TOP | LEFT | RIGHT}
visible
application={App}
child={
<CenterBox
orientation={Gtk.Orientation.HORIZONTAL}
startWidget={
<box
hexpand
halign={Gtk.Align.START}
>
<Hyprland.ModeStatus />
<Calendar.Time />
<Hyprland.Workspace />
</box>
}
centerWidget={<Hyprland.ActiveWindow />}
endWidget={
<box
hexpand
halign={Gtk.Align.END}
cssClasses={[ 'BarRight' ]}
>
<SystemInfo.SystemInfo />
<QuickView.QuickView />
</box>
}
></CenterBox>
}
></window>
);
};
const cliHandler = ( args: string[] ): string => {
console.debug( args );
return 'Not implemented';
};
const BarLauncher = ( monitor: Gdk.Monitor ) => {
const windowName = `bar-${ monitor.get_connector() }`;
const createBar = () => {
return <Bar gdkmonitor={monitor} name={windowName}></Bar>;
};
// Actually start the bar
createBar();
return windowName;
};
export default {
BarLauncher,
cliHandler,
};
+111
View File
@@ -0,0 +1,111 @@
@use "../../util/colours.scss" as *;
window.Bar {
font-family: "Comfortaa, sans-serif";
background: transparent;
color: $fg-color;
font-weight: bold;
/* >centerbox { */
/* background: $bg-color; */
/* border-radius: 10px; */
/* margin: 8px; */
/* } */
.mode-status {
color: white;
background-color: #00002dff;
padding-left: 10px;
padding-right: 10px;
margin-left: 5px;
border-radius: 20px;
font-family: $monospace-font;
&.command-mode {
background-color: darkslategray;
color: white;
}
&.windowing-mode {
background-color: darkslategray;
color: white;
}
&.workspace-mode {
background-color: darkblue;
color: white;
}
&.launch-mode {
background-color: darkgreen;
color: white;
}
&.device-mode {
background-color: darkred;
color: white;
}
&.screenshotting-mode {
background-color: purple;
color: white;
}
&.notifications-mode {
background-color: darkorange;
color: white;
}
}
.bar-button {
border-radius: 20px;
margin: 2px;
padding-left: 10px;
padding-right: 10px;
background-color: $bg-color;
& button {
background-color: $bg-color;
}
}
.quick-action-button {
border-radius: 20px;
margin: 2px;
padding-left: 10px;
padding-right: 10px;
background-color: $bg-color;
}
button.workspace-button {
border-radius: 20px;
margin: 1px;
&.focused-workspace-button {
color: $accent-color-2;
}
}
.tray-item {
margin: 0;
padding: 0;
& button {
margin: 2px;
box-shadow: none;
}
}
.time {
min-width: 11rem;
padding: 3px;
& button {
box-shadow: none;
padding: 0;
}
}
.quick-view-symbol {
margin: 4px;
}
}
@@ -0,0 +1,26 @@
import { GLib, Variable } from "astal";
import { Gtk } from "astal/gtk4";
const Time = ({ format = "%a, %e.%m %H:%M:%S" }) => {
const time = Variable<string>("").poll(
1000,
() => GLib.DateTime.new_now_local().format(format)!,
);
return (
<menubutton
cssClasses={["time", "bar-button"]}
hexpand
halign={Gtk.Align.CENTER}
>
<label onDestroy={() => time.drop()} label={time()} halign={Gtk.Align.CENTER}></label>
<popover>
<Gtk.Calendar />
</popover>
</menubutton>
);
};
export default {
Time,
};
@@ -0,0 +1,146 @@
import {
bind,
exec,
readFile
} from 'astal';
import AstalHyprland from 'gi://AstalHyprland';
import {
Gtk
} from 'astal/gtk4';
import Pango from 'gi://Pango?version=1.0';
const hypr = AstalHyprland.get_default();
const Workspace = () => {
return (
<box>
{bind( hypr, 'workspaces' ).as( wss => wss
.filter( ws => !( ws.id >= -99 && ws.id <= -2 ) ) // filter out special workspaces
.sort( ( a, b ) => a.id - b.id )
.map( ws => (
<button
cssClasses={bind( hypr, 'focusedWorkspace' ).as( fw => ws === fw
? [
'focused-workspace-button',
'workspace-button',
]
: [ 'workspace-button' ], )}
onButtonPressed={() => ws.focus()}
child={<label label={String( ws.id )}></label>}
></button>
) ), )}
</box>
);
};
/**
* Displays the name of the currently active window and provides a popover for
* displaying all available clients
*/
const ActiveWindow = () => {
const focused = bind( hypr, 'focusedClient' );
const WindowPopover = (): Gtk.Popover => {
// Set up boxes + Popover
const popover = new Gtk.Popover();
const popoverBox = WindowPopoverBox();
popover.set_child( popoverBox );
return popover;
};
const windowPopover = WindowPopover();
// ───────────────────────────────────────────────────────────────────
// Return fully assembled HyprlandFocusedClient box
// ───────────────────────────────────────────────────────────────────
return (
<box visible={focused.as( Boolean )}>
<button
onClicked={() => windowPopover.popup()}
cssClasses={[ 'bar-button' ]}
child={
focused.as( client => client && (
<label
maxWidthChars={70}
ellipsize={Pango.EllipsizeMode.END}
label={bind( client, 'title' ).as( String )} />
), )
}></button>
{windowPopover}
</box >
);
};
type submaps = 'device' | 'launch' | 'workspace' | 'windowing' | 'screenshotting' | 'notifications' | '';
const ModeStatus = () => {
let isUsingHyprvim = false;
try {
const path = exec( 'bash -c "cd ~ && pwd"' ) + '/.config/hyprvim';
isUsingHyprvim = readFile( path ).trim() === 'y';
} catch ( e ) {
printerr( 'Failed to read hyprvim config', e );
}
const label = new Gtk.Label();
if ( !isUsingHyprvim ) return label;
print( '==> Using hyprvim config' );
const map = {
'device': 'DEV',
'launch': 'LNC',
'workspace': 'WSP',
'windowing': 'WIN',
'screenshotting': 'SCS',
'notifications': 'NOT',
'': 'NRM'
};
label.label = map[''];
label.cssClasses = [ 'mode-status' ];
// TODO: Possibly add popover to it that lists binds
hypr.connect( 'submap', ( _, name: submaps ) => {
label.label = map[name];
label.cssClasses = [
'mode-status',
name + '-mode'
];
} );
return label;
};
const WindowPopoverBox = () => {
return <box vertical>
<label label={'Available Windows'} cssClasses={[ 'title-2' ]}></label>
<Gtk.Separator marginTop={5} marginBottom={5}></Gtk.Separator>
<box vertical>
{bind( hypr, 'clients' ).as( clients => {
return clients.map( client => {
return <button child={
<box>
<label label={bind( client, 'workspace' ).as( w => `(WS ${ w.name })` )}></label>
<label label={bind( client, 'initialClass' ).as( c => `[${ c }]` )}></label>
<label label={bind( client, 'title' )}></label>
</box>
}
onClicked={() => client.focus()}
></button>;
} );
} )}
</box>
</box>;
};
export default {
Workspace,
ActiveWindow,
ModeStatus
};
@@ -0,0 +1,204 @@
import AstalBattery from 'gi://AstalBattery';
import AstalBluetooth from 'gi://AstalBluetooth';
import AstalNetwork from 'gi://AstalNetwork';
import AstalWp from 'gi://AstalWp';
import Brightness from '../../../util/brightness';
import {
Gtk
} from 'astal/gtk4';
import QuickActions from '../../QuickActions/QuickActions';
import {
bind
} from 'astal';
import {
execAsync
} from 'astal';
const STATE = AstalNetwork.DeviceState;
const QuickView = () => {
const qa = QuickActions.QuickActions();
const showQuickActions = () => {
qa.popup();
};
return (
<button
onClicked={() => showQuickActions()}
cssClasses={[ 'quick-action-button' ]}
child={
<box>
<BatteryWidget></BatteryWidget>
<Audio></Audio>
<BluetoothWidget></BluetoothWidget>
<NetworkWidget></NetworkWidget>
<image iconName={'system-shutdown-symbolic'}></image>
{qa}
</box>
}
></button>
);
};
const NetworkWidget = () => {
const network = AstalNetwork.get_default();
return (
<box>
<image
iconName={bind( network, 'state' ).as( state => {
if ( state === AstalNetwork.State.CONNECTING ) {
return 'chronometer-reset-symbolic';
} else if (
state === AstalNetwork.State.CONNECTED_LOCAL
|| state === AstalNetwork.State.CONNECTED_SITE
|| state === AstalNetwork.State.CONNECTED_GLOBAL
) {
return 'network-wired-activated-symbolic';
} else {
return 'paint-unknown-symbolic';
}
} )}
cssClasses={[
'network-widget',
'quick-view-symbol'
]}
visible={bind( network.wifi, 'state' ).as( state => state !== STATE.ACTIVATED, )}
></image>
<image
iconName={bind( network.wifi, 'state' ).as( state => {
if ( state === STATE.ACTIVATED ) {
return network.wifi.iconName;
} else {
return '';
}
} )}
tooltipText={bind( network.wifi, 'ssid' )}
cssClasses={[
'network-widget',
'quick-view-symbol'
]}
visible={bind( network.wifi, 'state' ).as( state => state === STATE.ACTIVATED, )}
></image>
</box>
);
};
const BluetoothWidget = () => {
const bluetooth = AstalBluetooth.get_default();
const enabled = bind( bluetooth, 'isPowered' );
const connected = bind( bluetooth, 'isConnected' );
// For each connected BT device, render status
return (
<box>
<box visible={enabled.as( e => e )}>
<image
iconName={'bluetooth-active-symbolic'}
visible={connected.as( c => c )}
></image>
<image
iconName={'bluetooth-disconnected-symbolic'}
visible={connected.as( c => !c )}
></image>
</box>
<image
iconName={'bluetooth-disabled-symbolic'}
visible={enabled.as( e => !e )}
></image>
<box>
{bind( bluetooth, 'devices' ).as( devices => {
return devices.map( device => {
return (
<image
iconName={bind( device, 'icon' ).as( icon => icon, )}
visible={bind( device, 'connected' )}
tooltipText={bind( device, 'batteryPercentage' ).as( n => {
return device.get_name() + ': ' + n + '%';
}, )}
></image>
);
} );
} )}
</box>
</box>
);
};
let hasSentNotification = false;
const BatteryWidget = () => {
const battery = AstalBattery.get_default();
if ( battery.get_is_present() ) {
return (
<image
iconName={bind( battery, 'batteryIconName' ).as( icon => icon )}
cssClasses={[ 'quick-view-symbol' ]}
tooltipText={bind( battery, 'percentage' ).as( p => {
const level = Math.round( p * 100 );
if ( level < 20 && !hasSentNotification ) {
hasSentNotification = true;
execAsync( 'bash -c "notify-send \'Battery level below 20%\'"' );
}
return `Battery Level: ${ level }%`;
} )}
></image>
);
} else {
return <box></box>;
}
// Else, no battery available -> Don't show the widget
};
const BrightnessWidget = () => {
const brightness = Brightness.get_default();
const screen_brightness = bind( brightness, 'screen' );
return (
<box cssClasses={[ 'quick-view-symbol' ]}>
<image iconName={'brightness-high-symbolic'}></image>
<label
label={screen_brightness.as( b => '' + Math.round( 100 * b ) )}
visible={bind( brightness, 'screenAvailable' )}
></label>
</box>
);
};
const Audio = () => {
const wireplumber = AstalWp.get_default();
if ( wireplumber ) {
return (
<box orientation={Gtk.Orientation.HORIZONTAL}>
<image
iconName={bind( wireplumber.defaultSpeaker, 'volumeIcon' ).as( icon => icon, )}
cssClasses={[ 'quick-view-symbol' ]}
tooltipText={bind( wireplumber.defaultSpeaker, 'volume' ).as( v => Math.round( 100 * v ) + '%' )}
></image>
<image
iconName={bind( wireplumber.defaultMicrophone,
'volumeIcon', ).as( icon => icon )}
cssClasses={[ 'quick-view-symbol' ]}
tooltipText={bind( wireplumber.defaultMicrophone, 'volume' ).as( v => Math.round( 100 * v ) + '%' )}
></image>
</box>
);
} else {
print( '[ WirePlumber ] Could not connect, Audio support in bar will be missing', );
return <image iconName={'action-unavailable-symbolic'}></image>;
}
};
// cssClasses={[ 'quick-view-symbol' ]}
export default {
QuickView,
BrightnessWidget
};
@@ -0,0 +1,105 @@
import {
Gtk
} from 'astal/gtk4';
import {
execAsync
} from 'astal';
import sysinfo from '../sysinfo';
const info = () => {
return (
<box vertical>
<label
label={'System Information'}
cssClasses={[ 'title-2' ]}
></label>
<Gtk.Separator marginTop={5} marginBottom={10}></Gtk.Separator>
<label
vexpand
halign={Gtk.Align.START}
hexpand
label={sysinfo.ramUsed( used => {
return 'RAM: ' + used + ` (${ sysinfo.ramUtil.get() }%)`;
} )}
></label>
<label
label={sysinfo.systemStats( stats => {
return `CPU: ${ stats.cpuTemp }, ${ stats.cpuClk }
GPU: ${ stats.gpuTemp }, ${ stats.gpuClk } (${ stats.vram } / ${ stats.availableVRAM })
Kernel: ${ stats.kernel }`;
} )}
></label>
<Gtk.Separator marginTop={10}></Gtk.Separator>
<button
onClicked={() => execAsync( '/bin/sh -c "kitty --hold fish -c \'fastfetch\'"' )}
child={
<label label={'View FastFetch'}></label>
}></button>
</box>
);
};
const SystemInformationPanel = () => {
const popover = new Gtk.Popover();
popover.set_child( info() );
return popover;
};
const panel = SystemInformationPanel();
const SystemInfo = () => {
sysinfo.startSysInfoFetcher();
const openSysInfo = async () => {
panel.popup();
sysinfo.refreshStats();
};
if ( sysinfo.enabled ) {
return (
<button
onClicked={() => openSysInfo()}
child={
<box tooltipText={sysinfo.ramUsed( v => v )}>
<box
cssClasses={[ 'quick-view-symbol' ]}
>
<image
iconName={'power-profile-performance-symbolic'}
marginEnd={1}
></image>
<label
label={sysinfo.cpuUtil( util => util )}
marginEnd={5}
></label>
</box>
<box
cssClasses={[ 'quick-view-symbol' ]}
>
<image iconName={'memory'}></image>
<label label={sysinfo.ramUtil( util => util )}></label>
</box>
<box
cssClasses={[ 'quick-view-symbol' ]}
>
<image iconName={'show-gpu-effects-symbolic'}></image>
<label label={sysinfo.gpuUtil( util => util )}></label>
</box>
{panel}
</box>
}
cssClasses={[ 'bar-button' ]}
></button>
);
} else {
return <image iconName={'action-unavailable-symbolic'}></image>;
}
};
export default {
SystemInfo,
panel,
};
+9
View File
@@ -0,0 +1,9 @@
interface Stats {
kernel: string;
cpuTemp: string;
cpuClk: string;
gpuTemp: string;
gpuClk: string;
vram: string;
availableVRAM: string;
}
+137
View File
@@ -0,0 +1,137 @@
import { exec, execAsync, interval, Variable } from "astal";
const FETCH_INTERVAL = 2000;
const cpuUtil = Variable("0%");
const ramUtil = Variable("0%");
const ramUsed = Variable("0MiB");
const gpuUtil = Variable("0%");
let gpuName = "card1";
let enabled = true;
const getStats = (): Stats => {
gpuName = exec(`/bin/bash -c "ls /sys/class/drm/ | grep '^card[0-9]*$'"`);
const cpuNameInSensors = "CPUTIN";
const stats = {
kernel: exec("uname -sr"),
cpuTemp: exec(
`/bin/bash -c "sensors | grep -m1 ${cpuNameInSensors} | awk '{print $2}'"`,
),
cpuClk: exec(
`awk '/cpu MHz/ {sum+=$4; ++n} END {print sum/n " MHz"}' /proc/cpuinfo`,
),
gpuTemp: exec(
`/bin/bash -c "sensors | grep -E 'edge' | awk '{print $2}'"`,
),
gpuClk: exec(
`/bin/bash -c "cat /sys/class/drm/${gpuName}/device/pp_dpm_sclk | grep '\\*' | awk '{print $2 $3}'"`,
),
vram:
Math.round(
parseInt(
exec(
`cat /sys/class/drm/${gpuName}/device/mem_info_vram_used`,
),
) /
1024 /
1024,
) + "MiB",
availableVRAM:
Math.round(
parseInt(
exec(
`cat /sys/class/drm/${gpuName}/device/mem_info_vram_total`,
),
) /
1024 /
1024,
) + "MiB",
};
return stats;
};
const systemStats: Variable<Stats> = Variable(getStats());
const availableFeatures = {
cpu: true,
ram: true,
};
const refreshStats = () => {
systemStats.set(getStats());
}
const featureTest = () => {
print('[SysInfo] Feature test started...');
// Check if awk & sed are available
try {
exec("awk -V");
exec("sed --version");
} catch (e) {
printerr(
"[ SysInfo ] AWK or SED missing! No system info will be available",
);
enabled = false;
return;
}
// Check if mpstat is available
try {
exec("mpstat -V");
} catch (e) {
availableFeatures.cpu = false;
printerr(
"[ SysInfo ] Feature Test for CPU info failed. mpstat from the sysstat package missing!",
);
}
print('[SysInfo] Feature test complete');
};
const sysInfoFetcher = () => {
if (enabled) {
if (availableFeatures.cpu) {
execAsync(`/bin/fish -c cpu-utilization`).then(v => {
cpuUtil.set("" + Math.round(parseFloat(v)));
}).catch(e => {
console.error(e);
});
}
if (availableFeatures.ram) {
execAsync(
`/bin/bash -c "free | awk '/Mem:/ {print $3 \\" \\" $2}'"`,
).then(v => {
const util = parseInt(v.split(' ')[0]);
const available = parseInt(v.split(' ')[1]);
ramUtil.set("" + Math.round(util / available * 100));
ramUsed.set(`${Math.round(util / 1024 / 1024 * 10) / 10} GiB of ${Math.round(available / 1024 / 1024 * 10) / 10} GiB used`);
}).catch(e => {
console.error(e);
});
}
gpuUtil.set(exec("cat /sys/class/drm/card1/device/gpu_busy_percent"));
}
};
let sysInfoFetcherRunning = false;
const startSysInfoFetcher = () => {
if (!sysInfoFetcherRunning) {
sysInfoFetcherRunning = true;
featureTest();
if (enabled) {
// Start interval
interval(FETCH_INTERVAL, sysInfoFetcher);
}
}
}
export default {
startSysInfoFetcher,
enabled,
gpuUtil,
cpuUtil,
ramUsed,
ramUtil,
refreshStats,
systemStats
}
@@ -0,0 +1,87 @@
import { Variable } from "astal";
import { App, Astal, Gdk, Gtk, hook } from "astal/gtk4";
import AstalApps from "gi://AstalApps";
import AppList from "./modules/Apps";
const prefixes = ['='];
function hide() {
App.get_window("launcher")!.hide();
}
const Launcher = () => {
const apps = new AstalApps.Apps();
const width = Variable(1000);
const height = Variable(1000);
const text = Variable("");
const visible = Variable(false);
const onEnter = () => {
// TODO handle custom stuff
apps.fuzzy_query(text.get())?.[0].launch();
hide();
};
return <window
name="launcher"
visible={visible()}
anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM}
exclusivity={Astal.Exclusivity.EXCLUSIVE}
keymode={Astal.Keymode.ON_DEMAND}
application={App}
onShow={(self) => {
width.set(self.get_current_monitor().geometry.width);
height.set(self.get_current_monitor().geometry.height);
}}
onKeyPressed={(self, keyval) => {
if (keyval === Gdk.KEY_Escape) self.hide();
}}
child={
<box
vertical
cssClasses={["app-launcher-wrapper"]}
widthRequest={width()}
heightRequest={height()}
valign={Gtk.Align.CENTER}
>
<button onClicked={hide} visible={false} />
<box
vertical
cssClasses={["app-launcher"]}
valign={Gtk.Align.CENTER}
halign={Gtk.Align.CENTER}
widthRequest={500}
>
<button onClicked={hide} visible={false}></button>
<box cssClasses={["search"]}>
<image iconName={"system-search-symbolic"}></image>
<entry
placeholderText={"Search..."}
text={text.get()}
setup={self => {
hook(self, App, 'window-toggled', (_, win) => {
if (win.name == 'launcher') {
self.set_text('');
self.grab_focus();
}
})
}}
onNotifyText={self => text.set(self.text)}
primaryIconSensitive
onActivate={onEnter}
hexpand></entry>
</box>
<AppList
hide={hide}
query={text}
visible={text(v => {
return !prefixes.includes(v.slice(0, 1));
})}
></AppList>
</box>
</box>
}
>
</window>
}
export default Launcher;
@@ -0,0 +1,16 @@
@use '../../util/colours.scss' as *;
window {
background: transparent;
}
box.app-launcher-wrapper {
background-color: $shadow-color;
>box.app-launcher {
background-color: $bg-color;
border-radius: 30px;
padding: 20px;
border: 1px solid $accent-color-2;
}
}
@@ -0,0 +1,59 @@
import { Binding, Variable } from "astal";
import { Gtk } from "astal/gtk4";
import AstalApps from "gi://AstalApps";
import Pango from "gi://Pango?version=1.0";
const MAX_ITEMS = 8;
const AppList = ({ hide, query, visible }: { hide: () => void, query: Variable<string>, visible: Binding<Boolean> }) => {
const apps = new AstalApps.Apps();
const list = query((text) => apps.fuzzy_query(text).slice(0, MAX_ITEMS));
return <box>
<box
spacing={6}
vertical
cssClasses={["app-list"]}
visible={list.as(l => l.length > 0)}
>
{list.as(l => l.map(app => <AppButton app={app} hide={hide}></AppButton>))}
</box>
<box
halign={Gtk.Align.CENTER}
cssClasses={["list-empty"]}
vertical
visible={list.as(l => l.length === 0)}
>
<image iconName={"system-search-symbolic"}></image>
<label label={"No match found"}></label>
</box>
</box>
}
const AppButton = ({ app, hide }: { app: AstalApps.Application, hide: () => void }) => {
return <button
onClicked={() => {
hide();
app.launch();
}}
child={
<box>
<image iconName={app.iconName}></image>
<box valign={Gtk.Align.CENTER} vertical>
<label
cssClasses={["title-2"]}
ellipsize={Pango.EllipsizeMode.END}
maxWidthChars={40}
xalign={0}
label={app.name}
></label>
<label
wrap xalign={0}
label={app.description}
></label>
</box>
</box>
}>
</button>
}
export default AppList;
@@ -0,0 +1,6 @@
# Source
This has been copied from [matshell](https://github.com/Neurarian/matshell)
It is not yet used, as it has not been adapted yet to feature a notification history.
Potentially, a notification centre will be added to make this here work better. Styling is also missing
@@ -0,0 +1,32 @@
import { Astal } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import Hyprland from "gi://AstalHyprland";
import { bind } from "astal";
import { NotificationWidget } from "./modules/Notification";
import { hyprToGdk } from "../../util/hyprland";
export default function Notifications() {
const notifd = Notifd.get_default();
const hyprland = Hyprland.get_default();
const { TOP, RIGHT } = Astal.WindowAnchor;
return (
<window
name="notifications"
gdkmonitor={bind(hyprland, "focusedMonitor").as(
(focused: Hyprland.Monitor) => hyprToGdk(focused),
)}
anchor={TOP | RIGHT}
visible={bind(notifd, "notifications").as(
(notifications) => notifications.length > 0,
)}
child={
<box vertical={true} cssClasses={["notifications"]}>
{bind(notifd, "notifications").as((notifications) =>
notifications.map((n) => <NotificationWidget notification={n} />),
)}
</box>
}
/>
);
}
@@ -0,0 +1,25 @@
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { fileExists, isIcon } from "../../../util/notifd";
export function NotificationIcon(notification: Notifd.Notification) {
if (notification.image || notification.appIcon || notification.desktopEntry) {
const icon = notification.image || notification.appIcon || notification.desktopEntry;
if (fileExists(icon)) {
return (
<box expand={false} valign={Gtk.Align.CENTER}>
<image file={icon} />
</box>
);
} else if (isIcon(icon)) {
return (
<box expand={false} valign={Gtk.Align.CENTER}>
<image iconName={icon} />
</box>
);
}
}
return null;
}
@@ -0,0 +1,126 @@
import { bind } from "astal";
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { NotificationIcon } from "./Icon";
import { createTimeoutManager, time, urgency } from "../../../util/notifd";
export function NotificationWidget({
notification,
}: {
notification: Notifd.Notification;
}) {
const { START, CENTER, END } = Gtk.Align;
const actions = notification.actions || [];
const TIMEOUT_DELAY = 3000;
// Keep track of notification validity
const notifd = Notifd.get_default();
const timeoutManager = createTimeoutManager(
() => notification.dismiss(),
TIMEOUT_DELAY,
);
return (
<box
setup={(self) => {
// Set up timeout
timeoutManager.setupTimeout();
const clickGesture = Gtk.GestureClick.new();
clickGesture.set_button(0); // 0 means any button
clickGesture.connect("pressed", (gesture, _) => {
try {
// Get which button was pressed (1=left, 2=middle, 3=right)
const button = gesture.get_current_button();
if (button === 1) {
// PRIMARY/LEFT
if (actions.length > 0) n.invoke(actions[0]);
} else if (button === 2) {
// MIDDLE
notifd.notifications?.forEach((n) => {
n.dismiss();
});
} else if (button === 3) {
// SECONDARY/RIGHT
notification.dismiss();
}
} catch (error) {
console.error(error);
}
});
self.add_controller(clickGesture);
self.connect("unrealize", () => {
timeoutManager.cleanup();
});
}}
onHoverEnter={timeoutManager.handleHover}
onHoverLeave={timeoutManager.handleHoverLost}
vertical
vexpand={false}
cssClasses={["notification", `${urgency(notification)}`]}
name={notification.id.toString()}
>
<box cssClasses={["header"]}>
<label
cssClasses={["app-name"]}
halign={CENTER}
label={bind(notification, "app_name")}
/>
<label
cssClasses={["time"]}
hexpand
halign={END}
label={time(notification.time)}
/>
</box>
<Gtk.Separator />
<box cssClasses={["content"]}>
<box
cssClasses={["thumb"]}
visible={Boolean(NotificationIcon(notification))}
halign={CENTER}
valign={CENTER}
vexpand={true}
child={NotificationIcon(notification)!}
>
</box>
<box
vertical
cssClasses={["text-content"]}
hexpand={true}
halign={CENTER}
valign={CENTER}
>
<label
cssClasses={["title"]}
valign={CENTER}
wrap={false}
label={bind(notification, "summary")}
/>
{notification.body && (
<label
cssClasses={["body"]}
valign={CENTER}
wrap={true}
maxWidthChars={50}
label={bind(notification, "body")}
/>
)}
</box>
</box>
{actions.length > 0 && (
<box cssClasses={["actions"]}>
{actions.map(({ label, action }) => (
<button
hexpand
cssClasses={["action-button"]}
onClicked={() => notification.invoke(action)}
>
<label label={label} halign={CENTER} hexpand />
</button>
))}
</box>
)}
</box>
);
}
@@ -0,0 +1,6 @@
import AstalNotifd from "gi://AstalNotifd"
const notifd = AstalNotifd.get_default()
const getNotifications = () => {
}
@@ -0,0 +1,26 @@
import { Gtk } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import { fileExists, isIcon } from "./helper";
export function NotificationIcon(notification: Notifd.Notification) {
if ( notification.image || notification.appIcon || notification.desktopEntry) {
const icon = notification.image || notification.appIcon || notification.desktopEntry;
if (fileExists(icon)) {
return (
<box hexpand={false} valign={Gtk.Align.CENTER} child={
<image file={icon} />
}>
</box>
);
} else if (isIcon(icon)) {
return (
<box hexpand={false} valign={Gtk.Align.CENTER} child={
<image iconName={icon} />
}>
</box>
);
}
}
return null;
}
@@ -0,0 +1,32 @@
import { Astal } from "astal/gtk4";
import Notifd from "gi://AstalNotifd";
import Hyprland from "gi://AstalHyprland";
import { bind } from "astal";
import { NotificationWidget } from "./modules/Notification";
import { hyprToGdk } from "../../util/hyprland";
export default function Notifications() {
const notifd = Notifd.get_default();
const hyprland = Hyprland.get_default();
const { TOP, RIGHT } = Astal.WindowAnchor;
return (
<window
name="notifications"
gdkmonitor={bind(hyprland, "focusedMonitor").as(
(focused: Hyprland.Monitor) => hyprToGdk(focused),
)}
anchor={TOP | RIGHT}
visible={bind(notifd, "notifications").as(
(notifications) => notifications.length > 0,
)}
child={
<box vertical={true} cssClasses={["notifications"]}>
{bind(notifd, "notifications").as((notifications) =>
notifications.map((n) => <NotificationWidget notification={n} />),
)}
</box>
}
/>
);
}
@@ -0,0 +1,124 @@
@use "sass:string";
@function gtkalpha($c, $a) {
@return string.unquote("alpha(#{$c},#{$a})");
}
// https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-24/gtk/theme/Adwaita/_colors-public.scss
$fg-color: #{"@theme_fg_color"};
$bg-color: #{"@theme_bg_color"};
$error: red;
window.NotificationHandler {
all: unset;
}
box.notification {
&:first-child {
margin-top: 1rem;
}
&:last-child {
margin-bottom: 1rem;
}
& {
min-width: 400px;
border-radius: 13px;
background-color: $bg-color;
margin: .5rem 1rem .5rem 1rem;
box-shadow: 2px 3px 8px 0 gtkalpha(black, .4);
border: 1pt solid gtkalpha($fg-color, .03);
}
&.critical {
border: 1pt solid gtkalpha($error, .4);
.header {
.app-name {
color: gtkalpha($error, .8);
}
.app-icon {
color: gtkalpha($error, .6);
}
}
}
.header {
padding: .5rem;
color: gtkalpha($fg-color, 0.5);
.app-icon {
margin: 0 .4rem;
}
.app-name {
margin-right: .3rem;
font-weight: bold;
&:first-child {
margin-left: .4rem;
}
}
.time {
margin: 0 .4rem;
}
button {
padding: .2rem;
min-width: 0;
min-height: 0;
}
}
separator {
margin: 0 .4rem;
background-color: gtkalpha($fg-color, .1);
}
.content {
margin: 1rem;
margin-top: .5rem;
.summary {
font-size: 1.2em;
color: $fg-color;
}
.body {
color: gtkalpha($fg-color, 0.8);
}
.image {
border: 1px solid gtkalpha($fg-color, .02);
margin-right: .5rem;
border-radius: 9px;
min-width: 100px;
min-height: 100px;
background-size: cover;
background-position: center;
}
}
.actions {
margin: 1rem;
margin-top: 0;
button {
margin: 0 .3rem;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
}
}
+22
View File
@@ -0,0 +1,22 @@
declare const SRC: string
declare const DATADIR: string
declare module "inline:*" {
const content: string
export default content
}
declare module "*.scss" {
const content: string
export default content
}
declare module "*.blp" {
const content: string
export default content
}
declare module "*.css" {
const content: string
export default content
}
+28
View File
@@ -0,0 +1,28 @@
project('bar-launcher-tools', version: '1.0')
pkgdatadir = get_option('prefix') / get_option('datadir') / meson.project_name()
main = meson.project_name() + '.built'
custom_target(
command: [
find_program('ags'),
'bundle',
'--root', meson.project_source_root(),
meson.project_source_root() / 'app.ts',
main,
],
output: main,
input: files('app.ts'),
install: true,
install_dir: pkgdatadir,
)
configure_file(
input: files('wrapper.sh'),
output: meson.project_name(),
configuration: {
'MAIN_PROGRAM': pkgdatadir / main,
'LAYER_SHELL_LIBDIR': dependency('gtk4-layer-shell-0').get_variable('libdir'),
},
install: true,
install_dir: get_option('prefix') / get_option('bindir'),
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+6
View File
@@ -0,0 +1,6 @@
{
"name": "astal-shell",
"dependencies": {
"astal": "/usr/share/astal/gjs"
}
}
+24
View File
@@ -0,0 +1,24 @@
/* @use './components/notifications/notifications.scss'; */
@use "./components/bar/bar.scss";
@use "./components/QuickActions/quickactions.scss";
@use "./util/colours.scss" as *;
/* @use "./components/launcher/launcher.scss"; */
* {
font-size: 1rem;
}
empty {
min-width: 0;
background-color: transparent;
}
.title {
font-size: 1.5rem;
font-weight: bold;
}
.title-2 {
font-size: 1.2rem;
font-weight: bold;
}
+14
View File
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"experimentalDecorators": true,
"strict": true,
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
// "checkJs": true,
// "allowJs": true,
"jsx": "react-jsx",
"jsxImportSource": "astal/gtk4",
}
}
+82
View File
@@ -0,0 +1,82 @@
import GObject, { register, property } from "astal/gobject"
import { monitorFile, readFileAsync } from "astal/file"
import { exec, execAsync } from "astal/process"
const get = (args: string) => Number(exec(`brightnessctl ${args}`))
const screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`)
const kbd = exec(`bash -c "ls -w1 /sys/class/leds | head -1"`)
@register({ GTypeName: "Brightness" })
export default class Brightness extends GObject.Object {
static instance: Brightness
static get_default() {
if (!this.instance)
this.instance = new Brightness()
return this.instance
}
#kbdMax = get(`--device ${kbd} max`)
#kbd = get(`--device ${kbd} get`)
#screenMax = get("max")
#screen = get("get") / (get("max") || 1)
#screenAvailable = true
@property(Boolean)
get screenAvailable() { return this.#screenAvailable }
@property(Number)
get kbd() { return this.#kbd }
set kbd(value) {
if (value < 0 || value > this.#kbdMax)
return
execAsync(`brightnessctl -d ${kbd} s ${value} -q`).then(() => {
this.#kbd = value
this.notify("kbd")
})
}
@property(Number)
get screen() { return this.#screen }
set screen(percent) {
if (percent < 0)
percent = 0
if (percent > 1)
percent = 1
execAsync(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => {
this.#screen = percent
this.notify("screen")
})
}
constructor() {
super()
const screenPath = `/sys/class/backlight/${screen}/brightness`
const kbdPath = `/sys/class/leds/${kbd}/brightness`
monitorFile(screenPath, async f => {
const v = await readFileAsync(f)
this.#screen = Number(v) / this.#screenMax
this.notify("screen")
})
monitorFile(kbdPath, async f => {
const v = await readFileAsync(f)
this.#kbd = Number(v) / this.#kbdMax
this.notify("kbd")
})
// Check if there is a screen available
try {
get( 'g -c backlight' );
} catch ( _ ) {
this.#screenAvailable = false;
}
}
}
+6
View File
@@ -0,0 +1,6 @@
$fg-color: #E6E6E6;
$bg-color: #141414;
$accent-color: #5C9B3F;
$accent-color-2: #1674B9;
$shadow-color: rgba(40, 40, 40, 0.3);
$monospace-font: Source Code Pro

Some files were not shown because too many files have changed in this diff Show More