diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-06-14 22:50:55 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-14 22:50:55 +1000 |
| commit | ee7291b7f64a359d17eb1d050086ef5357d79055 (patch) | |
| tree | f1c200d8c8ba81030cbb2113a2be122db6508c8f | |
| parent | Merge pull request #5 from dalpax/patch-1 (diff) | |
| parent | Merge branch 'main' into python-rework (diff) | |
| download | caelestia-cli-ee7291b7f64a359d17eb1d050086ef5357d79055.tar.gz caelestia-cli-ee7291b7f64a359d17eb1d050086ef5357d79055.tar.bz2 caelestia-cli-ee7291b7f64a359d17eb1d050086ef5357d79055.zip | |
Merge pull request #6 from caelestia-dots/python-rework
feat: rewrite in python
86 files changed, 2313 insertions, 1651 deletions
@@ -1 +1,2 @@ -/data/schemes/dynamic/ +__pycache__/ +/dist/ @@ -1,34 +1,17 @@ -# caelestia-scripts +# caelestia-cli -A collection of scripts for my caelestia dotfiles. +The main control script for the Caelestia dotfiles. ## Installation -Clone this repo. +### Package manager -Run `install/scripts.fish`. -`~/.local/bin` must be in your path. +TODO -## Usage +### Manual installation -``` -> caelestia help -Usage: caelestia COMMAND [ ...args ] +TODO -COMMAND := help | install | shell | toggle | workspace-action | scheme | screenshot | record | clipboard | clipboard-delete | emoji-picker | wallpaper | pip +## Usage - help: show this help message - install: install a module - shell: start the shell or message it - toggle: toggle a special workspace - workspace-action: execute a Hyprland workspace dispatcher in the current group - scheme: change the current colour scheme - variant: change the current scheme variant - screenshot: take a screenshot - record: take a screen recording - clipboard: open clipboard history - clipboard-delete: delete an item from clipboard history - emoji-picker: open the emoji picker - wallpaper: change the wallpaper - pip: move the focused window into picture in picture mode or start the pip daemon -``` +TODO diff --git a/clipboard-delete.fish b/clipboard-delete.fish deleted file mode 100755 index b0212bb..0000000 --- a/clipboard-delete.fish +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env fish - -set -l chosen_item (cliphist list | fuzzel --dmenu --prompt='del > ' --placeholder='Delete from clipboard') -test -n "$chosen_item" && echo "$chosen_item" | cliphist delete diff --git a/clipboard.fish b/clipboard.fish deleted file mode 100755 index 579071d..0000000 --- a/clipboard.fish +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env fish - -set -l chosen_item (cliphist list | fuzzel --dmenu --placeholder='Type to search clipboard') -test -n "$chosen_item" && echo "$chosen_item" | cliphist decode | wl-copy diff --git a/completions/caelestia.fish b/completions/caelestia.fish index 448d05f..2be94d2 100644 --- a/completions/caelestia.fish +++ b/completions/caelestia.fish @@ -1,38 +1,35 @@ set -l seen '__fish_seen_subcommand_from' set -l has_opt '__fish_contains_opt' -set -l commands help install shell toggle workspace-action scheme variant screenshot record clipboard clipboard-delete emoji-picker wallpaper pip + +set -l commands shell toggle workspace-action scheme screenshot record clipboard emoji-picker wallpaper pip set -l not_seen "not $seen $commands" # Disable file completions complete -c caelestia -f +# Add help for any command +complete -c caelestia -s 'h' -l 'help' -d 'Show help' + # Subcommands -complete -c caelestia -n $not_seen -a 'help' -d 'Show help' -complete -c caelestia -n $not_seen -a 'install' -d 'Install a module' complete -c caelestia -n $not_seen -a 'shell' -d 'Start the shell or message it' complete -c caelestia -n $not_seen -a 'toggle' -d 'Toggle a special workspace' complete -c caelestia -n $not_seen -a 'workspace-action' -d 'Exec a dispatcher in the current group' -complete -c caelestia -n $not_seen -a 'scheme' -d 'Switch the current colour scheme' -complete -c caelestia -n $not_seen -a 'variant' -d 'Switch the current scheme variant' +complete -c caelestia -n $not_seen -a 'scheme' -d 'Manage the colour scheme' complete -c caelestia -n $not_seen -a 'screenshot' -d 'Take a screenshot' -complete -c caelestia -n $not_seen -a 'record' -d 'Take a screen recording' +complete -c caelestia -n $not_seen -a 'record' -d 'Start a screen recording' complete -c caelestia -n $not_seen -a 'clipboard' -d 'Open clipboard history' -complete -c caelestia -n $not_seen -a 'clipboard-delete' -d 'Delete from clipboard history' -complete -c caelestia -n $not_seen -a 'emoji-picker' -d 'Open the emoji picker' -complete -c caelestia -n $not_seen -a 'wallpaper' -d 'Change the wallpaper' +complete -c caelestia -n $not_seen -a 'emoji-picker' -d 'Toggle the emoji picker' +complete -c caelestia -n $not_seen -a 'wallpaper' -d 'Manage the wallpaper' complete -c caelestia -n $not_seen -a 'pip' -d 'Picture in picture utilities' -# Install -set -l commands all btop discord firefox fish foot fuzzel hypr safeeyes scripts shell slurp spicetify gtk qt vscode -complete -c caelestia -n "$seen install && not $seen $commands" -a "$commands" - # Shell -set -l commands help mpris drawers wallpaper notifs +set -l commands mpris drawers wallpaper notifs set -l not_seen "$seen shell && not $seen $commands" -complete -c caelestia -n $not_seen -a 'help' -d 'Show IPC commands' +complete -c caelestia -n $not_seen -s 's' -l 'show' -d 'Print all IPC commands' +complete -c caelestia -n $not_seen -s 'l' -l 'log' -d 'Print the shell log' complete -c caelestia -n $not_seen -a 'mpris' -d 'Mpris control' complete -c caelestia -n $not_seen -a 'drawers' -d 'Toggle drawers' -complete -c caelestia -n $not_seen -a 'wallpaper' -d 'Wallpaper control' +complete -c caelestia -n $not_seen -a 'wallpaper' -d 'Wallpaper control (for internal use)' complete -c caelestia -n $not_seen -a 'notifs' -d 'Notification control' set -l commands getActive play pause playPause stop next previous list @@ -81,53 +78,26 @@ set -l commands workspace workspacegroup movetoworkspace movetoworkspacegroup complete -c caelestia -n "$seen workspace-action && not $seen $commands" -a "$commands" -d 'action' # Scheme -set -q XDG_DATA_HOME && set -l data_dir $XDG_DATA_HOME || set -l data_dir $HOME/.local/share -set -l scheme_dir $data_dir/caelestia/scripts/data/schemes -set -l schemes (basename -a (find $scheme_dir/ -mindepth 1 -maxdepth 1 -type d)) -set -l commands 'print' $schemes -complete -c caelestia -n "$seen scheme && not $seen $commands" -a 'print' -d 'Generate and print a colour scheme for an image' -complete -c caelestia -n "$seen scheme && not $seen $commands" -a "$schemes" -d 'scheme' -for scheme in $schemes - set -l flavours (basename -a (find $scheme_dir/$scheme/ -mindepth 1 -maxdepth 1 -type d) 2> /dev/null) - set -l modes (basename -s .txt (find $scheme_dir/$scheme/ -mindepth 1 -maxdepth 1 -type f) 2> /dev/null) - if test -n "$modes" - complete -c caelestia -n "$seen scheme && $seen $scheme && not $seen $modes" -a "$modes" -d 'mode' - else - complete -c caelestia -n "$seen scheme && $seen $scheme && not $seen $flavours" -a "$flavours" -d 'flavour' - for flavour in $flavours - set -l modes (basename -s .txt (find $scheme_dir/$scheme/$flavour/ -mindepth 1 -maxdepth 1 -type f)) - complete -c caelestia -n "$seen scheme && $seen $scheme && $seen $flavour && not $seen $modes" -a "$modes" -d 'mode' - end - end -end - -# Variant -set -l commands vibrant tonalspot expressive fidelity fruitsalad rainbow neutral content monochrome -complete -c caelestia -n "$seen variant && not $seen $commands" -a "$commands" -d 'variant' +complete -c caelestia -n "$seen scheme" -s 'r' -l 'random' -d 'Switch to a random scheme' +complete -c caelestia -n "$seen scheme" -s 'n' -l 'name' -d 'Set scheme name' +complete -c caelestia -n "$seen scheme" -s 'f' -l 'flavour' -d 'Set scheme flavour' +complete -c caelestia -n "$seen scheme" -s 'm' -l 'mode' -d 'Set scheme mode' -a 'light dark' +complete -c caelestia -n "$seen scheme" -s 'v' -l 'variant' -d 'Set scheme variant' -a 'vibrant tonalspot expressive fidelity fruitsalad rainbow neutral content monochrome' # Record -set -l not_seen "$seen record && not $has_opt -s h help" -complete -c caelestia -n "$not_seen && not $has_opt -s s sound && not $has_opt -s r region && not $has_opt -s c compression && not $has_opt -s H hwaccel" \ - -s 'h' -l 'help' -d 'Show help' -complete -c caelestia -n "$not_seen && not $has_opt -s s sound" -s 's' -l 'sound' -d 'Capture sound' -complete -c caelestia -n "$not_seen && not $has_opt -s r region" -s 'r' -l 'region' -d 'Capture region' -complete -c caelestia -n "$not_seen && not $has_opt -s c compression" -s 'c' -l 'compression' -d 'Compression level of file' -r -complete -c caelestia -n "$not_seen && not $has_opt -s H hwaccel" -s 'H' -l 'hwaccel' -d 'Use hardware acceleration' +complete -c caelestia -n "$seen record" -s 'r' -l 'region' -d 'Capture region' +complete -c caelestia -n "$seen record" -s 's' -l 'sound' -d 'Capture sound' -# Wallpaper -set -l not_seen "$seen wallpaper && not $has_opt -s h help && not $has_opt -s f file && not $has_opt -s d directory" -complete -c caelestia -n $not_seen -s 'h' -l 'help' -d 'Show help' -complete -c caelestia -n $not_seen -s 'f' -l 'file' -d 'The file to switch to' -r -complete -c caelestia -n $not_seen -s 'd' -l 'directory' -d 'The directory to select from' -r - -complete -c caelestia -n "$seen wallpaper && $has_opt -s f file" -F -complete -c caelestia -n "$seen wallpaper && $has_opt -s d directory" -F +# Clipboard +complete -c caelestia -n "$seen clipboard" -s 'd' -l 'delete' -d 'Delete from cliboard history' -set -l not_seen "$seen wallpaper && $has_opt -s d directory && not $has_opt -s F no-filter && not $has_opt -s t threshold" -complete -c caelestia -n $not_seen -s 'F' -l 'no-filter' -d 'Do not filter by size' -complete -c caelestia -n $not_seen -s 't' -l 'threshold' -d 'The threshold to filter by' -r +# Wallpaper +complete -c caelestia -n "$seen wallpaper" -s 'p' -l 'print' -d 'Print the scheme for a wallpaper' -rF +complete -c caelestia -n "$seen wallpaper" -s 'r' -l 'random' -d 'Switch to a random wallpaper' -rF +complete -c caelestia -n "$seen wallpaper" -s 'f' -l 'file' -d 'The file to switch to' -rF +complete -c caelestia -n "$seen wallpaper" -s 'n' -l 'no-filter' -d 'Do not filter by size' +complete -c caelestia -n "$seen wallpaper" -s 't' -l 'threshold' -d 'The threshold to filter by' -r +complete -c caelestia -n "$seen wallpaper" -s 'N' -l 'no-smart' -d 'Disable smart mode switching' # Pip -set -l not_seen "$seen pip && not $has_opt -s h help && not $has_opt -s d daemon" -complete -c caelestia -n $not_seen -s 'h' -l 'help' -d 'Show help' -complete -c caelestia -n $not_seen -s 'd' -l 'daemon' -d 'Start in daemon mode' +complete -c caelestia -n "$seen pip" -s 'd' -l 'daemon' -d 'Start in daemon mode' diff --git a/data/config.json b/data/config.json deleted file mode 100644 index 47f61e5..0000000 --- a/data/config.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "toggles": { - "communication": { - "apps": [ - { - "selector": ".class == \"discord\"", - "spawn": "discord", - "action": "spawn move" - }, - { - "selector": ".class == \"whatsapp\"", - "spawn": "firefox --name whatsapp -P whatsapp 'https://web.whatsapp.com'", - "action": "move", - "extraCond": "grep -q 'Name=whatsapp' ~/.mozilla/firefox/profiles.ini" - } - ] - }, - "music": { - "apps": [ - { - "selector": ".class == \"Spotify\" or .initialTitle == \"Spotify\" or .initialTitle == \"Spotify Free\"", - "spawn": "spicetify watch -s", - "action": "spawn move" - }, - { - "selector": ".class == \"feishin\"", - "spawn": "feishin", - "action": "move" - } - ] - }, - "sysmon": { - "apps": [ - { - "selector": ".class == \"btop\" and .title == \"btop\" and .workspace.name == \"special:sysmon\"", - "spawn": "foot -a 'btop' -T 'btop' -- btop", - "action": "spawn" - } - ] - }, - "todo": { - "apps": [ - { - "selector": ".class == \"Todoist\"", - "spawn": "todoist", - "action": "spawn move" - } - ] - } - } -} diff --git a/emoji-picker.fish b/emoji-picker.fish deleted file mode 100755 index 4f2283c..0000000 --- a/emoji-picker.fish +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env fish - -set -l chosen_item (cat (dirname (status filename))/data/emojis.txt | fuzzel --dmenu --placeholder='Type to search emojis') -test -n "$chosen_item" && echo "$chosen_item" | cut -d ' ' -f 1 | tr -d '\n' | wl-copy diff --git a/install/btop.fish b/install/btop.fish deleted file mode 100755 index 3b3bcb4..0000000 --- a/install/btop.fish +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git btop - -set -l dist $CONFIG/btop - -# Update/Clone repo -update-repo btop $dist -sed -i 's|$SRC|'$dist'|g' $dist/btop.conf - -# Install systemd service -setup-systemd-monitor btop $dist - -log 'Done.' diff --git a/install/discord.fish b/install/discord.fish deleted file mode 100755 index a01538f..0000000 --- a/install/discord.fish +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git discord equicord-installer-bin -sudo Equilotl -install -location /opt/discord -sudo Equilotl -install-openasar -location /opt/discord - -set -l dist $C_DATA/discord - -# Update/Clone repo -update-repo discord $dist - -# Install systemd service -setup-systemd-monitor discord $dist - -# Link themes to client configs -set -l clients Vencord Equicord discord vesktop equibop legcord $argv -for client in $clients - if test -d $CONFIG/$client - log "Linking themes for $client" - install-link $dist/themes $CONFIG/$client/themes - end -end - -log 'Done.' diff --git a/install/firefox.fish b/install/firefox.fish deleted file mode 100755 index 5458fdd..0000000 --- a/install/firefox.fish +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git inotify-tools - -set -l dist $C_DATA/firefox - -# Update/Clone repo -update-repo firefox $dist - -# Install native app manifest -for dev in mozilla zen - if test -d $HOME/.$dev - mkdir -p $HOME/.$dev/native-messaging-hosts - cp $dist/native_app/manifest.json $HOME/.$dev/native-messaging-hosts/caelestiafox.json - sed -i "s|\$SRC|$dist|g" $HOME/.$dev/native-messaging-hosts/caelestiafox.json - end -end - -# Install zen css -if test -d $HOME/.zen - for profile in $HOME/.zen/*/chrome - for file in userChrome userContent - if test -f $profile/$file.css - set -l imp "@import url('$dist/zen/$file.css');" - grep -qFx $imp $profile/$file.css || printf '%s\n%s' $imp "$(cat $profile/$file.css)" > $profile/$file.css - else - echo "@import url('$dist/zen/$file.css');" > $profile/$file.css - end - end - end -end - -log 'Done.' -log 'Please install the extension manually from https://addons.mozilla.org/en-US/firefox/addon/caelestiafox' diff --git a/install/fish.fish b/install/fish.fish deleted file mode 100755 index 2bcc292..0000000 --- a/install/fish.fish +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git starship fastfetch - -set -l dist $C_DATA/fish - -# Update/Clone repo -update-repo fish $dist - -# Install fish config -install-link $dist/config.fish $CONFIG/fish/config.fish - -# Install fish greeting -install-link $dist/fish_greeting.fish $CONFIG/fish/functions/fish_greeting.fish - -# Install starship config -install-link $dist/starship.toml $CONFIG/starship.toml - -# Install fastfetch config -install-link $dist/fastfetch.jsonc $CONFIG/fastfetch/config.jsonc - -log 'Done.' diff --git a/install/foot.fish b/install/foot.fish deleted file mode 100755 index 67e682b..0000000 --- a/install/foot.fish +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git foot inotify-tools - -set -l dist $CONFIG/foot - -update-repo foot $dist -sed -i 's|$SRC|'$dist'|g' $dist/foot.ini - -install-link $dist/foot.fish ~/.local/bin/foot - -log 'Done.' diff --git a/install/fuzzel.fish b/install/fuzzel.fish deleted file mode 100755 index 915e46b..0000000 --- a/install/fuzzel.fish +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git fuzzel-git - -set -l dist $CONFIG/fuzzel - -# Clone repo -update-repo fuzzel $dist - -# Install systemd service -setup-systemd-monitor fuzzel $dist - -log 'Done.' diff --git a/install/gtk.fish b/install/gtk.fish deleted file mode 100755 index d1c999f..0000000 --- a/install/gtk.fish +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git adw-gtk-theme -install-optional-deps 'papirus-icon-theme (icon theme)' - -set -l dist $C_DATA/gtk - -# Update/Clone repo -update-repo gtk $dist - -# Install systemd service -setup-systemd-monitor gtk $dist - -# Set theme -gsettings set org.gnome.desktop.interface gtk-theme \'adw-gtk3-dark\' -if pacman -Q papirus-icon-theme &> /dev/null && test "$(gsettings get org.gnome.desktop.interface icon-theme | cut -d - -f 1 | string sub -s 2)" != Papirus - read -l -p "input 'Set icon theme to Papirus? [Y/n] ' -n" confirm - test "$confirm" = 'n' -o "$confirm" = 'N' || gsettings set org.gnome.desktop.interface icon-theme \'Papirus-Dark\' -end - -log 'Done.' diff --git a/install/hypr.fish b/install/hypr.fish deleted file mode 100755 index 44d2f5a..0000000 --- a/install/hypr.fish +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git uwsm hyprland-git hyprpaper-git hyprlock-git hypridle-git polkit-gnome gnome-keyring wl-clipboard wireplumber app2unit-git -install-optional-deps 'gammastep (night light)' 'wlogout (secondary session menu)' 'grimblast-git (screenshot freeze)' 'hyprpicker-git (colour picker)' 'foot (terminal emulator)' 'firefox (web browser)' 'vscodium-bin (IDE)' 'thunar (file manager)' 'nemo (secondary file manager)' 'fuzzel (secondary app launcher)' 'ydotool (alternate paste)' 'trash-cli (auto trash)' - -set -l hypr $CONFIG/hypr - -# Cause hyprland autogenerates a config file when it is removed -set -l remote https://github.com/caelestia-dots/hypr.git -if test -d $hypr - cd $hypr || exit - if test "$(git config --get remote.origin.url)" != $remote - cd .. || exit - confirm-overwrite $hypr dummy - git clone $remote /tmp/caelestia-hypr - rm -rf $hypr && mv /tmp/caelestia-hypr $hypr - else - git pull - end -else - git clone $remote $dir -end - -# Install uwsm envs -install-link $hypr/uwsm $CONFIG/uwsm - -# Enable ydotool if installed -pacman -Q ydotool &> /dev/null && systemctl --user enable --now ydotool.service - -log 'Done.' diff --git a/install/qt.fish b/install/qt.fish deleted file mode 100755 index 08ed1a0..0000000 --- a/install/qt.fish +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git darkly-bin -install-optional-deps 'papirus-icon-theme (icon theme)' - -set -l dist $C_DATA/qt - -# Update/Clone repo -update-repo qt $dist - -# Install systemd service -setup-systemd-monitor qt $dist - -# Change settings -confirm-copy $dist/qtct.conf $CONFIG/qt5ct/qt5ct.conf -confirm-copy $dist/qtct.conf $CONFIG/qt6ct/qt6ct.conf - -log 'Done.' diff --git a/install/safeeyes.fish b/install/safeeyes.fish deleted file mode 100755 index bbea62b..0000000 --- a/install/safeeyes.fish +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git dart-sass aylurs-gtk-shell-git alsa-utils libappindicator-gtk3 - -# Update/Clone repo -update-repo safeeyes $C_DATA/safeeyes - -if which systemctl &> /dev/null - log 'Installing systemd service...' - - set -l systemd $CONFIG/systemd/user - mkdir -p $systemd - echo -n " -[Unit] -Description=Protect your eyes from eye strain using this simple and beautiful, yet extensible break reminder. -After=graphical-session.target - -[Service] -Type=exec -ExecStart=/usr/bin/ags run -d $C_DATA/safeeyes -Restart=on-failure -Slice=app-graphical.slice - -[Install] -WantedBy=graphical-session.target -" > $systemd/caelestia-safeeyes.service - - systemctl --user daemon-reload - systemctl --user enable --now caelestia-safeeyes.service -end - -log 'Done.' diff --git a/install/scripts.fish b/install/scripts.fish deleted file mode 100755 index 5d91388..0000000 --- a/install/scripts.fish +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git hyprland-git hyprpaper-git imagemagick wl-clipboard fuzzel-git socat foot jq python xdg-user-dirs python-materialyoucolor-git app2unit-git grim wayfreeze-git wl-screenrec swappy -install-optional-deps 'discord (messaging app)' 'btop (system monitor)' 'zen-browser (web browser)' - -set -l dist $C_DATA/scripts - -# Update/Clone repo -update-repo scripts $dist - -# Install to path -install-link $dist/main.fish ~/.local/bin/caelestia - -# Install completions -test -e $CONFIG/fish/completions/caelestia.fish && rm $CONFIG/fish/completions/caelestia.fish -mkdir -p $CONFIG/fish/completions -cp $dist/completions/caelestia.fish $CONFIG/fish/completions/caelestia.fish - -log 'Done.' diff --git a/install/shell.fish b/install/shell.fish deleted file mode 100755 index e857c18..0000000 --- a/install/shell.fish +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -if ! pacman -Q lm_sensors > /dev/null - sudo pacman -S --noconfirm lm_sensors - sudo sensors-detect --auto -end - -install-deps git quickshell curl jq ttf-material-symbols-variable-git ttf-jetbrains-mono-nerd ttf-ibm-plex app2unit-git fd fish python-aubio python-pyaudio python-numpy cava networkmanager bluez-utils ddcutil brightnessctl imagemagick -install-optional-deps 'uwsm (for systems using uwsm)' - -set -l shell $C_DATA/shell - -# Update/Clone repo -update-repo shell $shell - -if which systemctl &> /dev/null - log 'Installing systemd service...' - - set -l systemd $CONFIG/systemd/user - mkdir -p $systemd - echo -n " -[Unit] -Description=A very segsy desktop shell. -After=graphical-session.target - -[Service] -Type=exec -ExecStart=$shell/run.fish -Restart=on-failure -Slice=app-graphical.slice - -[Install] -WantedBy=graphical-session.target -" > $systemd/caelestia-shell.service - - systemctl --user daemon-reload - systemctl --user enable --now caelestia-shell.service -end - -log 'Done.' diff --git a/install/slurp.fish b/install/slurp.fish deleted file mode 100755 index 56be19e..0000000 --- a/install/slurp.fish +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git slurp - -set -l dist $C_DATA/slurp - -# Clone repo -update-repo slurp $dist - -# Install systemd service -setup-systemd-monitor slurp $dist - -# Install to path -install-link $dist/slurp ~/.local/bin/slurp - -log 'Done.' diff --git a/install/spicetify.fish b/install/spicetify.fish deleted file mode 100755 index 912371b..0000000 --- a/install/spicetify.fish +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git spicetify-cli spicetify-marketplace-bin - -set -l dist $C_DATA/spicetify - -# Clone repo -update-repo spicetify $dist - -# Install systemd service -setup-systemd-monitor spicetify $dist - -# Install theme files -mkdir -p $CONFIG/spicetify/Themes/caelestia -cp $dist/color.ini $CONFIG/spicetify/Themes/caelestia/color.ini -cp $dist/user.css $CONFIG/spicetify/Themes/caelestia/user.css - -# Set spicetify theme -spicetify config current_theme caelestia color_scheme caelestia - -# Setup marketplace -spicetify config custom_apps marketplace diff --git a/install/util.fish b/install/util.fish deleted file mode 100644 index 9b258e0..0000000 --- a/install/util.fish +++ /dev/null @@ -1,148 +0,0 @@ -. (dirname (status filename))/../util.fish - -function confirm-overwrite -a path - if test -e $path -o -L $path - read -l -p "input '$(realpath $path 2> /dev/null || echo $path) already exists. Overwrite? [y/N] ' -n" confirm - if test "$confirm" = 'y' -o "$confirm" = 'Y' - log 'Continuing.' - test -z "$argv[2]" && rm -rf $path # If a second arg is provided, don't delete - else - log 'Exiting.' - exit - end - end -end - -function install-deps - # All dependencies already installed - pacman -Q $argv &> /dev/null && return - - for dep in $argv - # Skip if already installed - if ! pacman -Q $dep &> /dev/null - # If pacman can install it, use it, otherwise use an AUR helper - if pacman -Si $dep &> /dev/null - log "Installing dependency '$dep'" - sudo pacman -S --noconfirm $dep - else - # Get AUR helper or install if none - which yay &> /dev/null && set -l helper yay || set -l helper paru - if ! which $helper &> /dev/null - warn 'No AUR helper found' - read -l -p "input 'Install yay? [Y/n] ' -n" confirm - if test "$confirm" = 'n' -o "$confirm" = 'N' - warn "Manually install yay or paru and try again." - warn "Alternatively, install the dependencies '$argv' manually and try again." - exit - else - sudo pacman -S --needed git base-devel - git clone https://aur.archlinux.org/yay.git - cd yay - makepkg -si - cd .. - rm -rf yay - - # First use, see https://github.com/Jguer/yay?tab=readme-ov-file#first-use - yay -Y --gendb - yay -Y --devel --save - end - end - - log "Installing dependency '$dep'" - $helper -S --noconfirm $dep - end - end - end -end - -function install-optional-deps - for dep in $argv - set -l dep_name (string split -f 1 ' ' $dep) - if ! pacman -Q $dep_name &> /dev/null - read -l -p "input 'Install $dep? [Y/n] ' -n" confirm - test "$confirm" != 'n' -a "$confirm" != 'N' && install-deps $dep_name - end - end -end - -function update-repo -a module dir - set -l remote https://github.com/caelestia-dots/$module.git - if test -d $dir - cd $dir || exit - - # Delete and clone if it's a different git repo - if test "$(git config --get remote.origin.url)" != $remote - cd .. || exit - confirm-overwrite $dir - git clone $remote $dir - else - # Check for uncommitted changes - if test -n "$(git status --porcelain)" - read -l -p "input 'You have uncommitted changes in $dir. Stash, reset or exit? [S/r/e] ' -n" confirm - - if test "$confirm" = 'e' -o "$confirm" = 'E' - log 'Exiting...' - exit - end - - if test "$confirm" = 'r' -o "$confirm" = 'R' - log 'Resetting to HEAD...' - git reset --hard - else - log 'Stashing changes...' - git stash - end - end - - git pull - end - else - git clone $remote $dir - end -end - -function setup-systemd-monitor -a module dir - set -l systemd $CONFIG/systemd/user - if which systemctl &> /dev/null - log 'Installing systemd service...' - - mkdir -p $systemd - echo "[Unit] -Description=Sync $module and caelestia schemes - -[Service] -Type=oneshot -ExecStart=$dir/monitor/update.fish" > $systemd/$module-monitor-scheme.service - echo "[Unit] -Description=Sync $module and caelestia schemes (monitor) - -[Path] -PathModified=%S/caelestia/scheme/current.txt -Unit=$module-monitor-scheme.service - -[Install] -WantedBy=default.target" > $systemd/$module-monitor-scheme.path - - systemctl --user daemon-reload - systemctl --user enable --now $module-monitor-scheme.path - systemctl --user start $module-monitor-scheme.service - end -end - -function install-link -a from to - if ! test -L $to -a "$(realpath $to 2> /dev/null)" = $from - mkdir -p (dirname $to) - confirm-overwrite $to - ln -s $from $to - end -end - -function confirm-copy -a from to - test -L $to -a "$(realpath $to 2> /dev/null)" = (realpath $from) && return # Return if symlink - cmp $from $to &> /dev/null && return # Return if files are the same - if test -e $to - read -l -p "input '$(realpath $to) already exists. Overwrite? [y/N] ' -n" confirm - test "$confirm" = 'y' -o "$confirm" = 'Y' && log 'Continuing.' || return - end - cp $from $to -end diff --git a/install/vscode.fish b/install/vscode.fish deleted file mode 100755 index 32b9fcb..0000000 --- a/install/vscode.fish +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -install-deps git - -set -l dist $C_DATA/vscode - -# Update/Clone repo -update-repo vscode $dist - -# Install settings -for prog in 'Code' 'Code - OSS' 'VSCodium' - set -l conf $CONFIG/$prog - if test -d $conf - confirm-copy $dist/settings.json $conf/User/settings.json - confirm-copy $dist/keybindings.json $conf/User/keybindings.json - end -end - -# Install extension -for prog in code code-insiders codium - if which $prog &> /dev/null - log "Installing extensions for '$prog'" - if ! contains 'catppuccin.catppuccin-vsc-icons' ($prog --list-extensions) - read -l -p "input 'Install catppuccin icons (for light/dark integration)? [Y/n] ' -n" confirm - test "$confirm" = 'n' -o "$confirm" = 'N' || $prog --install-extension catppuccin.catppuccin-vsc-icons - end - $prog --install-extension $dist/caelestia-vscode-integration/caelestia-vscode-integration-*.vsix - end -end - -log 'Done.' diff --git a/main.fish b/main.fish deleted file mode 100755 index 7522540..0000000 --- a/main.fish +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env fish - -set -l src (dirname (realpath (status filename))) - -. $src/util.fish - -if test "$argv[1]" = shell - # Start shell if no args - if test -z "$argv[2..]" - if qs list --all | grep "Config path: $C_DATA/shell/shell.qml" &> /dev/null - warn 'Shell already running' - else - $C_DATA/shell/run.fish - end - else - if test "$argv[2]" = help - qs -p $C_DATA/shell ipc show - exit - end - - if qs list --all | grep "Config path: $C_DATA/shell/shell.qml" &> /dev/null - qs -p $C_DATA/shell ipc call $argv[2..] - else - warn 'Shell unavailable' - end - end - exit -end - -if test "$argv[1]" = toggle - set -l valid_toggles communication music sysmon specialws todo - if contains -- "$argv[2]" $valid_toggles - if test $argv[2] = specialws - $src/toggles/specialws.fish - else - . $src/toggles/util.fish - toggle-workspace $argv[2] - end - else - error "Invalid toggle: $argv[2]" - end - - exit -end - -if test "$argv[1]" = workspace-action - $src/workspace-action.sh $argv[2..] - exit -end - -if test "$argv[1]" = scheme - if test "$argv[2]" = print - $src/scheme/gen-print-scheme.fish $argv[3..] - else - $src/scheme/main.fish $argv[2..] - end - exit -end - -if test "$argv[1]" = variant - set -l variants vibrant tonalspot expressive fidelity fruitsalad rainbow neutral content monochrome - if contains -- "$argv[2]" $variants - echo -n $argv[2] > $C_STATE/scheme/current-variant.txt - $src/scheme/gen-scheme.fish - else - error "Invalid variant: $argv[2]" - end - - exit -end - -if test "$argv[1]" = install - set -l valid_modules scripts btop discord firefox fish foot fuzzel hypr safeeyes shell slurp spicetify gtk qt vscode - if test "$argv[2]" = all - for module in $valid_modules - $src/install/$module.fish $argv[3..] - end - else - contains -- "$argv[2]" $valid_modules && $src/install/$argv[2].fish $argv[3..] || error "Invalid module: $argv[2]" - end - test -f $C_STATE/scheme/current.txt || $src/scheme/main.fish onedark # Init scheme after install or update - exit -end - -set -l valid_subcommands screenshot record clipboard clipboard-delete emoji-picker wallpaper pip - -if contains -- "$argv[1]" $valid_subcommands - $src/$argv[1].fish $argv[2..] - exit -end - -test "$argv[1]" != help && error "Unknown command: $argv[1]" - -echo 'Usage: caelestia COMMAND [ ...args ]' -echo -echo 'COMMAND := help | install | shell | toggle | workspace-action | scheme | screenshot | record | clipboard | clipboard-delete | emoji-picker | wallpaper | pip' -echo -echo ' help: show this help message' -echo ' install: install a module' -echo ' shell: start the shell or message it' -echo ' toggle: toggle a special workspace' -echo ' workspace-action: execute a Hyprland workspace dispatcher in the current group' -echo ' scheme: change the current colour scheme' -echo ' variant: change the current scheme variant' -echo ' screenshot: take a screenshot' -echo ' record: take a screen recording' -echo ' clipboard: open clipboard history' -echo ' clipboard-delete: delete an item from clipboard history' -echo ' emoji-picker: open the emoji picker' -echo ' wallpaper: change the wallpaper' -echo ' pip: move the focused window into picture in picture mode or start the pip daemon' - -# Set exit status -test "$argv[1]" = help -exit diff --git a/pip.fish b/pip.fish deleted file mode 100755 index 08fda6d..0000000 --- a/pip.fish +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env fish - -argparse -n 'caelestia-pip' -X 0 \ - 'h/help' \ - 'd/daemon' \ - -- $argv -or exit - -if set -q _flag_h - echo 'Usage:' - echo ' caelestia pip ( -h | --help )' - echo ' caelestia pip [ -d | --daemon ]' - echo - echo 'Options:' - echo ' -h, --help Print this help message and exit' - echo ' -d, --daemon Run this script in daemon mode' - echo - echo 'Normal mode (no args):' - echo ' Move and resize the active window to picture in picture default geometry.' - echo - echo 'Daemon mode:' - echo ' Set all picture in picture window initial geometry to default.' - - exit -end - -. (dirname (status filename))/util.fish - -function handle-window -a address workspace - set -l monitor_id (hyprctl workspaces -j | jq '.[] | select(.name == "'$workspace'").monitorID') - set -l monitor_size (hyprctl monitors -j | jq -r '.[] | select(.id == '$monitor_id') | "\(.width)\n\(.height)"') - set -l window_size (hyprctl clients -j | jq '.[] | select(.address == "'$address'").size[]') - set -l scale_factor (math $monitor_size[2] / 4 / $window_size[2]) - set -l scaled_window_size (math -s 0 $window_size[1] x $scale_factor) (math -s 0 $window_size[2] x $scale_factor) - - hyprctl dispatch "resizewindowpixel exact $scaled_window_size,address:$address" > /dev/null - hyprctl dispatch "movewindowpixel exact $(math -s 0 $monitor_size[1] x 0.98 - $scaled_window_size[1]) $(math -s 0 $monitor_size[2] x 0.97 - $scaled_window_size[2]),address:$address" > /dev/null - log "Handled window at address $address" -end - -if set -q _flag_d - log 'Daemon started' - socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read line - switch $line - case 'openwindow*' - set -l window (string sub -s 13 $line | string split ',') - if string match -qr '^(Picture(-| )in(-| )[Pp]icture)$' $window[4] - handle-window 0x$window[1] $window[2] - end - end - end - - exit -end - -set -l active_window (hyprctl activewindow -j | jq -r '"\(.address)\n\(.workspace.name)\n\(.floating)"') -if test $active_window[3] = true - handle-window $active_window -else - warn 'Focused window is not floating, ignoring' -end diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..61c689a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "caelestia" +requires-python = ">=3.13" +dynamic = ["version"] + +[project.scripts] +caelestia = "caelestia:main" + +[tool.hatch.version] +source = "vcs" diff --git a/record.fish b/record.fish deleted file mode 100755 index d7c292d..0000000 --- a/record.fish +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env fish - -function get-audio-source - pactl list short sources | grep '\.monitor.*RUNNING' | cut -f 2 | head -1 -end - -function get-region - slurp || exit 0 -end - -function get-active-monitor - hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name' -end - -argparse -n 'caelestia-record' -X 0 \ - 'h/help' \ - 's/sound' \ - 'r/region=?' \ - 'n/no-hwaccel' \ - -- $argv -or exit - -if set -q _flag_h - echo 'Usage:' - echo ' caelestia record ( -h | --help )' - echo ' caelestia record [ -s | --sound ] [ -r | --region ] [ -c | --compression ] [ -H | --hwaccel ]' - echo - echo 'Options:' - echo ' -h, --help Print this help message and exit' - echo ' -s, --sound Enable audio capturing' - echo ' -r, --region [ <region> ] The region to capture, current monitor if option not given, default region slurp' - echo ' -N, --no-hwaccel Do not use the GPU encoder' - - exit -end - -. (dirname (status filename))/util.fish - -set -l storage_dir (xdg-user-dir VIDEOS)/Recordings -set -l state_dir $C_STATE/record - -mkdir -p $storage_dir -mkdir -p $state_dir - -set -l file_ext 'mp4' -set -l recording_path "$state_dir/recording.$file_ext" -set -l notif_id_path "$state_dir/notifid.txt" - -if pgrep wl-screenrec > /dev/null - pkill wl-screenrec - - # Move to recordings folder - set -l new_recording_path "$storage_dir/recording_$(date '+%Y%m%d_%H-%M-%S').$file_ext" - mv $recording_path $new_recording_path - - # Close start notification - if test -f $notif_id_path - gdbus call --session \ - --dest org.freedesktop.Notifications \ - --object-path /org/freedesktop/Notifications \ - --method org.freedesktop.Notifications.CloseNotification \ - (cat $notif_id_path) - end - - # Notification with actions - set -l action (notify-send 'Recording stopped' "Stopped recording $new_recording_path" -i 'video-x-generic' -a 'caelestia-record' \ - --action='watch=Watch' --action='open=Open' --action='save=Save As' --action='delete=Delete') - - switch $action - case 'watch' - app2unit -O $new_recording_path - case 'open' - dbus-send --session --dest=org.freedesktop.FileManager1 --type=method_call /org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems array:string:"file://$new_recording_path" string:'' \ - || app2unit -O (dirname $new_recording_path) - case 'save' - set -l save_file (app2unit -- zenity --file-selection --save --title='Save As') - test -n "$save_file" && mv $new_recording_path $save_file || warn 'No file selected' - case 'delete' - rm $new_recording_path - end -else - # Set region if flag given otherwise active monitor - if set -q _flag_r - # Use given region if value otherwise slurp - set region -g (test -n "$_flag_r" && echo $_flag_r || get-region) - else - set region -o (get-active-monitor) - end - - # Sound if enabled - set -q _flag_s && set -l audio --audio --audio-device (get-audio-source) - - # No hardware accel - set -q _flag_n && set -l hwaccel --no-hw - - wl-screenrec $region $audio $hwaccel --codec hevc -f $recording_path & disown - - notify-send 'Recording started' 'Recording...' -i 'video-x-generic' -a 'caelestia-record' -p > $notif_id_path -end @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +# Utility script for rebuilding and running caelestia + +cd $(dirname $0) || exit + +sudo rm -r dist /usr/bin/caelestia /usr/lib/python3.*/site-packages/caelestia* 2> /dev/null +python -m build --wheel --no-isolation > /dev/null +sudo python -m installer --destdir=/ dist/*.whl > /dev/null + +/usr/bin/caelestia "$@" diff --git a/scheme/autoadjust.py b/scheme/autoadjust.py deleted file mode 100755 index 1c6dc2c..0000000 --- a/scheme/autoadjust.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python3 - -import sys -from colorsys import hls_to_rgb, rgb_to_hls -from pathlib import Path - -from materialyoucolor.blend import Blend -from materialyoucolor.dynamiccolor.material_dynamic_colors import ( - DynamicScheme, - MaterialDynamicColors, -) -from materialyoucolor.hct import Hct -from materialyoucolor.scheme.scheme_content import SchemeContent -from materialyoucolor.scheme.scheme_expressive import SchemeExpressive -from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity -from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad -from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome -from materialyoucolor.scheme.scheme_neutral import SchemeNeutral -from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow -from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot -from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant - -light_colours = [ - "dc8a78", - "dd7878", - "ea76cb", - "8839ef", - "d20f39", - "e64553", - "fe640b", - "df8e1d", - "40a02b", - "179299", - "04a5e5", - "209fb5", - "1e66f5", - "7287fd", -] - -dark_colours = [ - "f5e0dc", - "f2cdcd", - "f5c2e7", - "cba6f7", - "f38ba8", - "eba0ac", - "fab387", - "f9e2af", - "a6e3a1", - "94e2d5", - "89dceb", - "74c7ec", - "89b4fa", - "b4befe", -] - -colour_names = [ - "rosewater", - "flamingo", - "pink", - "mauve", - "red", - "maroon", - "peach", - "yellow", - "green", - "teal", - "sky", - "sapphire", - "blue", - "lavender", - "success", - "error", -] - -HLS = tuple[float, float, float] - - -def hex_to_rgb(hex: str) -> tuple[float, float, float]: - """Convert a hex string to an RGB tuple in the range [0, 1].""" - return tuple(int(hex[i : i + 2], 16) / 255 for i in (0, 2, 4)) - - -def rgb_to_hex(rgb: tuple[float, float, float]) -> str: - """Convert an RGB tuple in the range [0, 1] to a hex string.""" - return "".join(f"{round(i * 255):02X}" for i in rgb) - - -def hex_to_hls(hex: str) -> tuple[float, float, float]: - return rgb_to_hls(*hex_to_rgb(hex)) - - -def hls_to_hex(h: str, l: str, s: str) -> str: - return rgb_to_hex(hls_to_rgb(h, l, s)) - - -def hex_to_argb(hex: str) -> int: - return int(f"0xFF{hex}", 16) - - -def argb_to_hls(argb: int) -> HLS: - return hex_to_hls(f"{argb:08X}"[2:]) - - -def grayscale(hls: HLS, light: bool) -> HLS: - h, l, s = hls - return h, 0.5 - l / 2 if light else l / 2 + 0.5, 0 - - -def mix(a: HLS, b: HLS, w: float) -> HLS: - r1, g1, b1 = hls_to_rgb(*a) - r2, g2, b2 = hls_to_rgb(*b) - return rgb_to_hls( - r1 * (1 - w) + r2 * w, g1 * (1 - w) + g2 * w, b1 * (1 - w) + b2 * w - ) - - -def harmonize(a: str, b: int) -> HLS: - return argb_to_hls(Blend.harmonize(hex_to_argb(a), b)) - - -def darken(colour: HLS, amount: float) -> HLS: - h, l, s = colour - return h, max(0, l - amount), s - - -def distance(colour: HLS, base: str) -> float: - h1, l1, s1 = colour - h2, l2, s2 = hex_to_hls(base) - return abs(h1 - h2) * 0.4 + abs(l1 - l2) * 0.3 + abs(s1 - s2) * 0.3 - - -def smart_sort(colours: list[HLS], base: list[str]) -> dict[str, HLS]: - sorted_colours = [None] * len(colours) - distances = {} - - for colour in colours: - dist = [(i, distance(colour, b)) for i, b in enumerate(base)] - dist.sort(key=lambda x: x[1]) - distances[colour] = dist - - for colour in colours: - while len(distances[colour]) > 0: - i, dist = distances[colour][0] - - if sorted_colours[i] is None: - sorted_colours[i] = colour, dist - break - elif sorted_colours[i][1] > dist: - old = sorted_colours[i][0] - sorted_colours[i] = colour, dist - colour = old - - distances[colour].pop(0) - - return {colour_names[i]: c[0] for i, c in enumerate(sorted_colours)} - - -def get_scheme(scheme: str) -> DynamicScheme: - if scheme == "content": - return SchemeContent - if scheme == "expressive": - return SchemeExpressive - if scheme == "fidelity": - return SchemeFidelity - if scheme == "fruitsalad": - return SchemeFruitSalad - if scheme == "monochrome": - return SchemeMonochrome - if scheme == "neutral": - return SchemeNeutral - if scheme == "rainbow": - return SchemeRainbow - if scheme == "tonalspot": - return SchemeTonalSpot - return SchemeVibrant - - -def get_alt(i: int) -> str: - names = ["default", "alt1", "alt2"] - return names[i] - - -if __name__ == "__main__": - light = sys.argv[1] == "light" - scheme = sys.argv[2] - primaries = sys.argv[3].split(" ") - colours_in = sys.argv[4].split(" ") - out_path = sys.argv[5] - - base = light_colours if light else dark_colours - - # Convert to HLS - base_colours = [hex_to_hls(c) for c in colours_in] - - # Sort colours and turn into dict - base_colours = smart_sort(base_colours, base) - - # Adjust colours - MatScheme = get_scheme(scheme) - for name, hls in base_colours.items(): - if scheme == "monochrome": - base_colours[name] = grayscale(hls, light) - else: - argb = hex_to_argb(hls_to_hex(*hls)) - mat_scheme = MatScheme(Hct.from_int(argb), not light, 0) - - colour = MaterialDynamicColors.primary.get_hct(mat_scheme) - - # Boost neutral scheme colours - if scheme == "neutral": - colour.chroma += 10 - - base_colours[name] = hex_to_hls( - "{:02X}{:02X}{:02X}".format(*colour.to_rgba()[:3]) - ) - - # Layers and accents - for i, primary in enumerate(primaries): - material = {} - - primary_argb = hex_to_argb(primary) - primary_scheme = MatScheme(Hct.from_int(primary_argb), not light, 0) - for colour in vars(MaterialDynamicColors).keys(): - colour_name = getattr(MaterialDynamicColors, colour) - if hasattr(colour_name, "get_hct"): - rgb = colour_name.get_hct(primary_scheme).to_rgba()[:3] - material[colour] = hex_to_hls("{:02X}{:02X}{:02X}".format(*rgb)) - - # TODO: eventually migrate to material for layers - colours = { - **material, - "text": material["onBackground"], - "subtext1": material["onSurfaceVariant"], - "subtext0": material["outline"], - "overlay2": mix(material["surface"], material["outline"], 0.86), - "overlay1": mix(material["surface"], material["outline"], 0.71), - "overlay0": mix(material["surface"], material["outline"], 0.57), - "surface2": mix(material["surface"], material["outline"], 0.43), - "surface1": mix(material["surface"], material["outline"], 0.29), - "surface0": mix(material["surface"], material["outline"], 0.14), - "base": material["surface"], - "mantle": darken(material["surface"], 0.03), - "crust": darken(material["surface"], 0.05), - "success": harmonize(base[8], primary_argb), - } - - for name, hls in base_colours.items(): - colours[name] = harmonize(hls_to_hex(*hls), primary_argb) - - out_file = Path(f"{out_path}/{scheme}/{get_alt(i)}/{sys.argv[1]}.txt") - out_file.parent.mkdir(parents=True, exist_ok=True) - colour_arr = [ - f"{name} {hls_to_hex(*colour)}" for name, colour in colours.items() - ] - out_file.write_text("\n".join(colour_arr)) diff --git a/scheme/gen-print-scheme.fish b/scheme/gen-print-scheme.fish deleted file mode 100755 index 6dfa482..0000000 --- a/scheme/gen-print-scheme.fish +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env fish - -set -l src (dirname (status filename)) - -. $src/../util.fish - -test -f "$argv[1]" && set -l img (realpath "$argv[1]") || set -l img $C_STATE/wallpaper/thumbnail.jpg - -# Thumbnail image if not already thumbnail -if test $img != $C_STATE/wallpaper/thumbnail.jpg - set -l thumb_path $C_CACHE/thumbnails/(sha1sum $img | cut -d ' ' -f 1).jpg - if ! test -f $thumb_path - magick -define jpeg:size=256x256 $img -thumbnail 128x128\> $thumb_path - end - set img $thumb_path -end - -set -l variants vibrant tonalspot expressive fidelity fruitsalad rainbow neutral content monochrome -contains -- "$argv[2]" $variants && set -l variant $argv[2] || set -l variant (cat $C_STATE/scheme/current-variant.txt 2> /dev/null) -contains -- "$variant" $variants || set -l variant tonalspot - -set -l hash (sha1sum $img | cut -d ' ' -f 1) - -# Cache scheme -if ! test -d $C_CACHE/schemes/$hash/$variant - set -l colours ($src/score.py $img) - $src/autoadjust.py dark $variant $colours $C_CACHE/schemes/$hash - $src/autoadjust.py light $variant $colours $C_CACHE/schemes/$hash -end - -# Get mode from image -set -l lightness (magick $img -format '%[fx:int(mean*100)]' info:) -test $lightness -ge 60 && set -l mode light || set -l mode dark - -# Print scheme -cat $C_CACHE/schemes/$hash/$variant/default/$mode.txt diff --git a/scheme/gen-scheme.fish b/scheme/gen-scheme.fish deleted file mode 100755 index 35e0bc5..0000000 --- a/scheme/gen-scheme.fish +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env fish - -set -l src (dirname (status filename)) - -. $src/../util.fish - -test -f "$argv[1]" && set -l img (realpath "$argv[1]") || set -l img $C_STATE/wallpaper/thumbnail.jpg - -set -l variants vibrant tonalspot expressive fidelity fruitsalad rainbow neutral content monochrome -contains -- "$argv[2]" $variants && set -l variant $argv[2] || set -l variant (cat $C_STATE/scheme/current-variant.txt 2> /dev/null) -contains -- "$variant" $variants || set -l variant tonalspot - -set -l hash (sha1sum $img | cut -d ' ' -f 1) - -# Cache scheme -if ! test -d $C_CACHE/schemes/$hash/$variant - set -l colours ($src/score.py $img) - $src/autoadjust.py dark $variant $colours $C_CACHE/schemes/$hash - $src/autoadjust.py light $variant $colours $C_CACHE/schemes/$hash -end - -# Copy scheme from cache -rm -rf $src/../data/schemes/dynamic -cp -r $C_CACHE/schemes/$hash/$variant $src/../data/schemes/dynamic - -# Update if current -set -l variant (string match -gr 'dynamic-(.*)' (cat $C_STATE/scheme/current-name.txt 2> /dev/null)) -if test -n "$variant" - # If variant doesn't exist, use default - test -d $src/../data/schemes/dynamic/$variant || set -l variant default - # Apply scheme - $src/main.fish dynamic $variant $MODE > /dev/null -end diff --git a/scheme/main.fish b/scheme/main.fish deleted file mode 100755 index 5e6ad03..0000000 --- a/scheme/main.fish +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env fish - -# Usage: -# caelestia scheme <scheme> <flavour> [mode] -# caelestia scheme <scheme> [flavour] -# caelestia scheme [scheme] - -function set-scheme -a path name mode - mkdir -p $C_STATE/scheme - - # Update scheme colours - cp $path $C_STATE/scheme/current.txt - - # Update scheme name - echo -n $name > $C_STATE/scheme/current-name.txt - - # Update scheme mode - echo -n $mode > $C_STATE/scheme/current-mode.txt - - log "Changed scheme to $name ($mode)" -end - -set -l src (dirname (status filename))/.. -set -l schemes $src/data/schemes - -. $src/util.fish - -set -l scheme $argv[1] -set -l flavour $argv[2] -set -l mode $argv[3] - -set -l valid_schemes (basename -a $schemes/*) - -test -z "$scheme" && set -l scheme (random choice $valid_schemes) - -if contains -- "$scheme" $valid_schemes - set -l flavours (basename -a (find $schemes/$scheme/ -mindepth 1 -maxdepth 1 -type d) 2> /dev/null) - set -l modes (basename -s .txt (find $schemes/$scheme/ -mindepth 1 -maxdepth 1 -type f) 2> /dev/null) - - if test -n "$modes" - # Scheme only has one flavour, so second arg is mode - set -l mode $flavour - if test -z "$mode" - # Try to use current mode if not provided and current mode exists for flavour, otherwise random mode - set mode (cat $C_STATE/scheme/current-mode.txt 2> /dev/null) - contains -- "$mode" $modes || set mode (random choice $modes) - end - - if contains -- "$mode" $modes - # Provided valid mode - set-scheme $schemes/$scheme/$mode.txt $scheme $mode - else - error "Invalid mode for $scheme: $mode" - end - else - # Scheme has multiple flavours, so second arg is flavour - test -z "$flavour" && set -l flavour (random choice $flavours) - - if contains -- "$flavour" $flavours - # Provided valid flavour - set -l modes (basename -s .txt $schemes/$scheme/$flavour/*.txt) - if test -z "$mode" - # Try to use current mode if not provided and current mode exists for flavour, otherwise random mode - set mode (cat $C_STATE/scheme/current-mode.txt 2> /dev/null) - contains -- "$mode" $modes || set mode (random choice $modes) - end - - if contains -- "$mode" $modes - # Provided valid mode - set-scheme $schemes/$scheme/$flavour/$mode.txt $scheme-$flavour $mode - else - error "Invalid mode for $scheme $flavour: $mode" - end - else - # Invalid flavour - error "Invalid flavour for $scheme: $flavour" - end - end -else - error "Invalid scheme: $scheme" -end diff --git a/screenshot.fish b/screenshot.fish deleted file mode 100755 index 122ad3e..0000000 --- a/screenshot.fish +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env fish - -. (dirname (status filename))/util.fish - -mkdir -p "$C_CACHE/screenshots" -set -l tmp_file "$C_CACHE/screenshots/$(date +'%Y%m%d%H%M%S')" - -if test "$argv[1]" = 'region' - if test "$argv[2]" = 'freeze' - wayfreeze --hide-cursor & set PID $last_pid - sleep .1 - end - - set -l ws (hyprctl -j activeworkspace | jq -r '.id') - set -l region (hyprctl -j clients | jq -r --argjson activeWsId $ws '.[] | select(.workspace.id == $activeWsId) | "\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"' | slurp) - if test -n "$region" - grim -l 0 -g $region - | swappy -f - & - end - - set -q PID && kill $PID - - exit -end - -grim $argv $tmp_file; and wl-copy < $tmp_file; or exit 1 - -set -l action (notify-send -i 'image-x-generic-symbolic' -h "STRING:image-path:$tmp_file" \ - -a 'caelestia-screenshot' --action='open=Open' --action='save=Save' \ - 'Screenshot taken' "Screenshot stored in $tmp_file and copied to clipboard") -switch $action - case 'open' - app2unit -- swappy -f $tmp_file & disown - case 'save' - set -l save_file (app2unit -- zenity --file-selection --save --title='Save As') - test -z $save_file && exit 0 - if test -f $save_file - app2unit -- yad --image='abrt' --title='Warning!' --text-align='center' --buttons-layout='center' --borders=20 \ - --text='<span size="x-large">Are you sure you want to overwrite this file?</span>' || exit 0 - end - cp $tmp_file $save_file -end diff --git a/src/caelestia/__init__.py b/src/caelestia/__init__.py new file mode 100644 index 0000000..71c9b62 --- /dev/null +++ b/src/caelestia/__init__.py @@ -0,0 +1,13 @@ +from caelestia.parser import parse_args + + +def main() -> None: + parser, args = parse_args() + if "cls" in args: + args.cls(args).run() + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/data/emojis.txt b/src/caelestia/data/emojis.txt index 3d929c7..3d929c7 100644 --- a/data/emojis.txt +++ b/src/caelestia/data/emojis.txt diff --git a/data/schemes/catppuccin/frappe/dark.txt b/src/caelestia/data/schemes/catppuccin/frappe/dark.txt index 1502230..1502230 100644 --- a/data/schemes/catppuccin/frappe/dark.txt +++ b/src/caelestia/data/schemes/catppuccin/frappe/dark.txt diff --git a/data/schemes/catppuccin/latte/light.txt b/src/caelestia/data/schemes/catppuccin/latte/light.txt index 6cc0fce..6cc0fce 100644 --- a/data/schemes/catppuccin/latte/light.txt +++ b/src/caelestia/data/schemes/catppuccin/latte/light.txt diff --git a/data/schemes/catppuccin/macchiato/dark.txt b/src/caelestia/data/schemes/catppuccin/macchiato/dark.txt index 6ffb12f..6ffb12f 100644 --- a/data/schemes/catppuccin/macchiato/dark.txt +++ b/src/caelestia/data/schemes/catppuccin/macchiato/dark.txt diff --git a/data/schemes/catppuccin/mocha/dark.txt b/src/caelestia/data/schemes/catppuccin/mocha/dark.txt index 66497d0..66497d0 100644 --- a/data/schemes/catppuccin/mocha/dark.txt +++ b/src/caelestia/data/schemes/catppuccin/mocha/dark.txt diff --git a/src/caelestia/data/schemes/dynamic/alt1/dark.txt b/src/caelestia/data/schemes/dynamic/alt1/dark.txt new file mode 100644 index 0000000..f3c70ea --- /dev/null +++ b/src/caelestia/data/schemes/dynamic/alt1/dark.txt @@ -0,0 +1,81 @@ +primary_paletteKeyColor 5E8046 +secondary_paletteKeyColor 6E7B62 +tertiary_paletteKeyColor 517F7E +neutral_paletteKeyColor 75786F +neutral_variant_paletteKeyColor 74796D +background 11140E +onBackground E1E4D9 +surface 11140E +surfaceDim 11140E +surfaceBright 373A33 +surfaceContainerLowest 0C0F09 +surfaceContainerLow 191D16 +surfaceContainer 1D211A +surfaceContainerHigh 282B24 +surfaceContainerHighest 33362F +onSurface E1E4D9 +surfaceVariant 44483E +onSurfaceVariant C4C8BB +inverseSurface E1E4D9 +inverseOnSurface 2E312A +outline 8E9286 +outlineVariant 44483E +shadow 000000 +scrim 000000 +surfaceTint ACD28F +primary ACD28F +onPrimary 1A3705 +primaryContainer 304F1A +onPrimaryContainer C7EEA9 +inversePrimary 476730 +secondary BDCBAF +onSecondary 283420 +secondaryContainer 414D37 +onSecondaryContainer D9E7CA +tertiary A0CFCE +onTertiary 003737 +tertiaryContainer 6B9998 +onTertiaryContainer 000000 +error FFB4AB +onError 690005 +errorContainer 93000A +onErrorContainer FFDAD6 +primaryFixed C7EEA9 +primaryFixedDim ACD28F +onPrimaryFixed 0A2000 +onPrimaryFixedVariant 304F1A +secondaryFixed D9E7CA +secondaryFixedDim BDCBAF +onSecondaryFixed 141E0C +onSecondaryFixedVariant 3F4A35 +tertiaryFixed BBECEA +tertiaryFixedDim A0CFCE +onTertiaryFixed 002020 +onTertiaryFixedVariant 1E4E4D +text E1E4D9 +subtext1 C4C8BB +subtext0 8E9286 +overlay2 7D8075 +overlay1 6A6D63 +overlay0 585C52 +surface2 474A42 +surface1 353931 +surface0 22261F +base 11140E +mantle 090B08 +crust 040503 +success ADE29A +rosewater ACD28F +flamingo 9BD4A0 +pink 8AD0EF +mauve 91CEF5 +red 86D6BE +maroon 81D4DA +peach 90D6AE +yellow A7D293 +green A3D398 +teal 82D5C7 +sky 80D5D3 +sapphire 86D2E8 +blue 9CCBFA +lavender 81D3E2
\ No newline at end of file diff --git a/src/caelestia/data/schemes/dynamic/alt1/light.txt b/src/caelestia/data/schemes/dynamic/alt1/light.txt new file mode 100644 index 0000000..84b0e64 --- /dev/null +++ b/src/caelestia/data/schemes/dynamic/alt1/light.txt @@ -0,0 +1,81 @@ +primary_paletteKeyColor 5E8046 +secondary_paletteKeyColor 6E7B62 +tertiary_paletteKeyColor 517F7E +neutral_paletteKeyColor 75786F +neutral_variant_paletteKeyColor 74796D +background F9FAF0 +onBackground 191D16 +surface F9FAF0 +surfaceDim D9DBD1 +surfaceBright F9FAF0 +surfaceContainerLowest FFFFFF +surfaceContainerLow F3F5EA +surfaceContainer EDEFE4 +surfaceContainerHigh E7E9DF +surfaceContainerHighest E1E4D9 +onSurface 191D16 +surfaceVariant E0E4D6 +onSurfaceVariant 44483E +inverseSurface 2E312A +inverseOnSurface F0F2E7 +outline 71766B +outlineVariant C4C8BB +shadow 000000 +scrim 000000 +surfaceTint 476730 +primary 476730 +onPrimary FFFFFF +primaryContainer C7EEA9 +onPrimaryContainer 304F1A +inversePrimary ACD28F +secondary 56624B +onSecondary FFFFFF +secondaryContainer D7E4C7 +onSecondaryContainer 3F4A35 +tertiary 4F7C7C +onTertiary FFFFFF +tertiaryContainer 4F7C7C +onTertiaryContainer FFFFFF +error BA1A1A +onError FFFFFF +errorContainer FFDAD6 +onErrorContainer 93000A +primaryFixed C7EEA9 +primaryFixedDim ACD28F +onPrimaryFixed 0A2000 +onPrimaryFixedVariant 304F1A +secondaryFixed D9E7CA +secondaryFixedDim BDCBAF +onSecondaryFixed 141E0C +onSecondaryFixedVariant 3F4A35 +tertiaryFixed BBECEA +tertiaryFixedDim A0CFCE +onTertiaryFixed 002020 +onTertiaryFixedVariant 1E4E4D +text 191D16 +subtext1 44483E +subtext0 71766B +overlay2 84887E +overlay1 989C92 +overlay0 ABAFA4 +surface2 BFC1B7 +surface1 D2D4C9 +surface0 E6E8DD +base F9FAF0 +mantle F4F6E5 +crust F1F4DD +success 4A9F23 +rosewater 3D6837 +flamingo 34693F +pink 006968 +mauve 00696F +red 156A59 +maroon 006876 +peach 256B4A +yellow 426733 +green 476730 +teal 00677B +sky 2E628B +sapphire 206486 +blue 0F6681 +lavender 0D6A5F
\ No newline at end of file diff --git a/src/caelestia/data/schemes/dynamic/alt2/dark.txt b/src/caelestia/data/schemes/dynamic/alt2/dark.txt new file mode 100644 index 0000000..9b36dee --- /dev/null +++ b/src/caelestia/data/schemes/dynamic/alt2/dark.txt @@ -0,0 +1,81 @@ +primary_paletteKeyColor 5E76AB +secondary_paletteKeyColor 70778B +tertiary_paletteKeyColor 8B6D8C +neutral_paletteKeyColor 76777D +neutral_variant_paletteKeyColor 757780 +background 121318 +onBackground E2E2E9 +surface 121318 +surfaceDim 121318 +surfaceBright 37393E +surfaceContainerLowest 0C0E13 +surfaceContainerLow 1A1B20 +surfaceContainer 1E1F25 +surfaceContainerHigh 282A2F +surfaceContainerHighest 33353A +onSurface E2E2E9 +surfaceVariant 44474F +onSurfaceVariant C5C6D0 +inverseSurface E2E2E9 +inverseOnSurface 2F3036 +outline 8E9099 +outlineVariant 44474F +shadow 000000 +scrim 000000 +surfaceTint AEC6FF +primary AEC6FF +onPrimary 122F60 +primaryContainer 2C4678 +onPrimaryContainer D8E2FF +inversePrimary 455E91 +secondary BFC6DC +onSecondary 293041 +secondaryContainer 3F4759 +onSecondaryContainer DBE2F9 +tertiary DFBBDE +onTertiary 402843 +tertiaryContainer A786A7 +onTertiaryContainer 000000 +error FFB4AB +onError 690005 +errorContainer 93000A +onErrorContainer FFDAD6 +primaryFixed D8E2FF +primaryFixedDim AEC6FF +onPrimaryFixed 001A43 +onPrimaryFixedVariant 2C4678 +secondaryFixed DBE2F9 +secondaryFixedDim BFC6DC +onSecondaryFixed 141B2C +onSecondaryFixedVariant 3F4759 +tertiaryFixed FCD7FB +tertiaryFixedDim DFBBDE +onTertiaryFixed 2A132D +onTertiaryFixedVariant 583E5A +text E2E2E9 +subtext1 C5C6D0 +subtext0 8E9099 +overlay2 7D7E87 +overlay1 6A6C74 +overlay0 595A62 +surface2 47494F +surface1 36373D +surface0 23242A +base 121318 +mantle 0B0C0F +crust 070709 +success 93E5B6 +rosewater 9BD4A1 +flamingo 84D5C3 +pink A1CAFE +mauve A5C8FF +red 80D3DE +maroon 8ECFF2 +peach 80D5D0 +yellow 93D5A9 +green 8DD5B3 +teal 84D2E5 +sky 89D0ED +sapphire 9CCBFB +blue ACC6FF +lavender 94CDF7
\ No newline at end of file diff --git a/src/caelestia/data/schemes/dynamic/alt2/light.txt b/src/caelestia/data/schemes/dynamic/alt2/light.txt new file mode 100644 index 0000000..00483f0 --- /dev/null +++ b/src/caelestia/data/schemes/dynamic/alt2/light.txt @@ -0,0 +1,81 @@ +primary_paletteKeyColor 5E76AB +secondary_paletteKeyColor 70778B +tertiary_paletteKeyColor 8B6D8C +neutral_paletteKeyColor 76777D +neutral_variant_paletteKeyColor 757780 +background FAF9FF +onBackground 1A1B20 +surface FAF9FF +surfaceDim DAD9E0 +surfaceBright FAF9FF +surfaceContainerLowest FFFFFF +surfaceContainerLow F3F3FA +surfaceContainer EEEDF4 +surfaceContainerHigh E8E7EF +surfaceContainerHighest E2E2E9 +onSurface 1A1B20 +surfaceVariant E1E2EC +onSurfaceVariant 44474F +inverseSurface 2F3036 +inverseOnSurface F1F0F7 +outline 72747D +outlineVariant C5C6D0 +shadow 000000 +scrim 000000 +surfaceTint 455E91 +primary 455E91 +onPrimary FFFFFF +primaryContainer D8E2FF +onPrimaryContainer 2C4678 +inversePrimary AEC6FF +secondary 575E71 +onSecondary FFFFFF +secondaryContainer DBE2F9 +onSecondaryContainer 3F4759 +tertiary 896B8A +onTertiary FFFFFF +tertiaryContainer 896B8A +onTertiaryContainer FFFFFF +error BA1A1A +onError FFFFFF +errorContainer FFDAD6 +onErrorContainer 93000A +primaryFixed D8E2FF +primaryFixedDim AEC6FF +onPrimaryFixed 001A43 +onPrimaryFixedVariant 2C4678 +secondaryFixed DBE2F9 +secondaryFixedDim BFC6DC +onSecondaryFixed 141B2C +onSecondaryFixedVariant 3F4759 +tertiaryFixed FCD7FB +tertiaryFixedDim DFBBDE +onTertiaryFixed 2A132D +onTertiaryFixedVariant 583E5A +text 1A1B20 +subtext1 44474F +subtext0 72747D +overlay2 85878F +overlay1 999BA3 +overlay0 ACADB5 +surface2 C0C0C7 +surface1 D3D2D9 +surface0 E7E6ED +base FAF9FF +mantle EDEAFF +crust E5E0FF +success 00A25A +rosewater 1F6A4E +flamingo 056A5C +pink 15667E +mauve 1B6685 +red 006972 +maroon 266389 +peach 006A67 +yellow 2B6A46 +green 35693F +teal 30628C +sky 435E91 +sapphire 3D5F8F +blue 37608E +lavender 0A6777
\ No newline at end of file diff --git a/src/caelestia/data/schemes/dynamic/default/dark.txt b/src/caelestia/data/schemes/dynamic/default/dark.txt new file mode 100644 index 0000000..001e000 --- /dev/null +++ b/src/caelestia/data/schemes/dynamic/default/dark.txt @@ -0,0 +1,81 @@ +primary_paletteKeyColor 2E8195 +secondary_paletteKeyColor 647B82 +tertiary_paletteKeyColor 707598 +neutral_paletteKeyColor 72787A +neutral_variant_paletteKeyColor 70797C +background 0F1416 +onBackground DEE3E6 +surface 0F1416 +surfaceDim 0F1416 +surfaceBright 343A3C +surfaceContainerLowest 090F11 +surfaceContainerLow 171C1E +surfaceContainer 1B2022 +surfaceContainerHigh 252B2D +surfaceContainerHighest 303638 +onSurface DEE3E6 +surfaceVariant 3F484B +onSurfaceVariant BFC8CB +inverseSurface DEE3E6 +inverseOnSurface 2C3133 +outline 899295 +outlineVariant 3F484B +shadow 000000 +scrim 000000 +surfaceTint 85D2E7 +primary 85D2E7 +onPrimary 003641 +primaryContainer 004E5D +onPrimaryContainer AEECFF +inversePrimary 00687B +secondary B2CBD3 +onSecondary 1D343A +secondaryContainer 364D53 +onSecondaryContainer CEE7EF +tertiary BFC4EB +onTertiary 292E4D +tertiaryContainer 898FB3 +onTertiaryContainer 000000 +error FFB4AB +onError 690005 +errorContainer 93000A +onErrorContainer FFDAD6 +primaryFixed AEECFF +primaryFixedDim 85D2E7 +onPrimaryFixed 001F26 +onPrimaryFixedVariant 004E5D +secondaryFixed CEE7EF +secondaryFixedDim B2CBD3 +onSecondaryFixed 061F25 +onSecondaryFixedVariant 344A51 +tertiaryFixed DEE1FF +tertiaryFixedDim BFC4EB +onTertiaryFixed 141937 +onTertiaryFixedVariant 3F4565 +text DEE3E6 +subtext1 BFC8CB +subtext0 899295 +overlay2 788083 +overlay1 666D70 +overlay0 555C5E +surface2 434A4D +surface1 32393B +surface0 202628 +base 0F1416 +mantle 090C0D +crust 050607 +success 93E5B6 +rosewater 9BD4A1 +flamingo 84D5C3 +pink 8CD0F1 +mauve 91CEF5 +red 80D4DC +maroon 85D2E7 +peach 80D5D0 +yellow 93D5A9 +green 8DD5B3 +teal 81D3E0 +sky 83D2E4 +sapphire 8AD1EE +blue 9CCBFA +lavender 86D1EB
\ No newline at end of file diff --git a/src/caelestia/data/schemes/dynamic/default/light.txt b/src/caelestia/data/schemes/dynamic/default/light.txt new file mode 100644 index 0000000..09648cf --- /dev/null +++ b/src/caelestia/data/schemes/dynamic/default/light.txt @@ -0,0 +1,81 @@ +primary_paletteKeyColor 2E8195 +secondary_paletteKeyColor 647B82 +tertiary_paletteKeyColor 707598 +neutral_paletteKeyColor 72787A +neutral_variant_paletteKeyColor 70797C +background F5FAFC +onBackground 171C1E +surface F5FAFC +surfaceDim D5DBDD +surfaceBright F5FAFC +surfaceContainerLowest FFFFFF +surfaceContainerLow EFF4F7 +surfaceContainer E9EFF1 +surfaceContainerHigh E4E9EB +surfaceContainerHighest DEE3E6 +onSurface 171C1E +surfaceVariant DBE4E7 +onSurfaceVariant 3F484B +inverseSurface 2C3133 +inverseOnSurface ECF2F4 +outline 6D7679 +outlineVariant BFC8CB +shadow 000000 +scrim 000000 +surfaceTint 00687B +primary 00687B +onPrimary FFFFFF +primaryContainer AEECFF +onPrimaryContainer 004E5D +inversePrimary 85D2E7 +secondary 4B6269 +onSecondary FFFFFF +secondaryContainer CEE7EF +onSecondaryContainer 344A51 +tertiary 6D7395 +onTertiary FFFFFF +tertiaryContainer 6D7395 +onTertiaryContainer FFFFFF +error BA1A1A +onError FFFFFF +errorContainer FFDAD6 +onErrorContainer 93000A +primaryFixed AEECFF +primaryFixedDim 85D2E7 +onPrimaryFixed 001F26 +onPrimaryFixedVariant 004E5D +secondaryFixed CEE7EF +secondaryFixedDim B2CBD3 +onSecondaryFixed 061F25 +onSecondaryFixedVariant 344A51 +tertiaryFixed DEE1FF +tertiaryFixedDim BFC4EB +onTertiaryFixed 141937 +onTertiaryFixedVariant 3F4565 +text 171C1E +subtext1 3F484B +subtext0 6D7679 +overlay2 80888B +overlay1 949C9F +overlay0 A7AFB1 +surface2 BBC1C4 +surface1 CED4D6 +surface0 E2E8EA +base F5FAFC +mantle E9F4F8 +crust E1F0F6 +success 00A25A +rosewater 1F6A4E +flamingo 056A5C +pink 046877 +mauve 00687B +red 006970 +maroon 02677E +peach 006A67 +yellow 2B6A46 +green 35693F +teal 0D6680 +sky 2E628B +sapphire 206486 +blue 156583 +lavender 036873
\ No newline at end of file diff --git a/data/schemes/gruvbox/hard/dark.txt b/src/caelestia/data/schemes/gruvbox/hard/dark.txt index 06bd012..06bd012 100644 --- a/data/schemes/gruvbox/hard/dark.txt +++ b/src/caelestia/data/schemes/gruvbox/hard/dark.txt diff --git a/data/schemes/gruvbox/hard/light.txt b/src/caelestia/data/schemes/gruvbox/hard/light.txt index 89c65a8..89c65a8 100644 --- a/data/schemes/gruvbox/hard/light.txt +++ b/src/caelestia/data/schemes/gruvbox/hard/light.txt diff --git a/data/schemes/gruvbox/medium/dark.txt b/src/caelestia/data/schemes/gruvbox/medium/dark.txt index 1ed9168..1ed9168 100644 --- a/data/schemes/gruvbox/medium/dark.txt +++ b/src/caelestia/data/schemes/gruvbox/medium/dark.txt diff --git a/data/schemes/gruvbox/medium/light.txt b/src/caelestia/data/schemes/gruvbox/medium/light.txt index 0c484cf..0c484cf 100644 --- a/data/schemes/gruvbox/medium/light.txt +++ b/src/caelestia/data/schemes/gruvbox/medium/light.txt diff --git a/data/schemes/gruvbox/soft/dark.txt b/src/caelestia/data/schemes/gruvbox/soft/dark.txt index 5a952e7..5a952e7 100644 --- a/data/schemes/gruvbox/soft/dark.txt +++ b/src/caelestia/data/schemes/gruvbox/soft/dark.txt diff --git a/data/schemes/gruvbox/soft/light.txt b/src/caelestia/data/schemes/gruvbox/soft/light.txt index eae8b04..eae8b04 100644 --- a/data/schemes/gruvbox/soft/light.txt +++ b/src/caelestia/data/schemes/gruvbox/soft/light.txt diff --git a/data/schemes/oldworld/dark.txt b/src/caelestia/data/schemes/oldworld/default/dark.txt index 846dc18..846dc18 100644 --- a/data/schemes/oldworld/dark.txt +++ b/src/caelestia/data/schemes/oldworld/default/dark.txt diff --git a/data/schemes/onedark/dark.txt b/src/caelestia/data/schemes/onedark/default/dark.txt index 269096e..269096e 100644 --- a/data/schemes/onedark/dark.txt +++ b/src/caelestia/data/schemes/onedark/default/dark.txt diff --git a/data/schemes/rosepine/dawn/light.txt b/src/caelestia/data/schemes/rosepine/dawn/light.txt index 90f4f73..90f4f73 100644 --- a/data/schemes/rosepine/dawn/light.txt +++ b/src/caelestia/data/schemes/rosepine/dawn/light.txt diff --git a/data/schemes/rosepine/main/dark.txt b/src/caelestia/data/schemes/rosepine/main/dark.txt index 061454b..061454b 100644 --- a/data/schemes/rosepine/main/dark.txt +++ b/src/caelestia/data/schemes/rosepine/main/dark.txt diff --git a/data/schemes/rosepine/moon/dark.txt b/src/caelestia/data/schemes/rosepine/moon/dark.txt index 37183ae..37183ae 100644 --- a/data/schemes/rosepine/moon/dark.txt +++ b/src/caelestia/data/schemes/rosepine/moon/dark.txt diff --git a/data/schemes/shadotheme/dark.txt b/src/caelestia/data/schemes/shadotheme/default/dark.txt index e178804..e178804 100644 --- a/data/schemes/shadotheme/dark.txt +++ b/src/caelestia/data/schemes/shadotheme/default/dark.txt diff --git a/src/caelestia/data/templates/btop.theme b/src/caelestia/data/templates/btop.theme new file mode 100644 index 0000000..9e63bce --- /dev/null +++ b/src/caelestia/data/templates/btop.theme @@ -0,0 +1,83 @@ +# Main background, empty for terminal default, need to be empty if you want transparent background +theme[main_bg]={{ $surface }} + +# Main text color +theme[main_fg]={{ $onSurface }} + +# Title color for boxes +theme[title]={{ $onSurface }} + +# Highlight color for keyboard shortcuts +theme[hi_fg]={{ $primary }} + +# Background color of selected item in processes box +theme[selected_bg]={{ $surfaceContainer }} + +# Foreground color of selected item in processes box +theme[selected_fg]={{ $primary }} + +# Color of inactive/disabled text +theme[inactive_fg]={{ $outline }} + +# Color of text appearing on top of graphs, i.e uptime and current network graph scaling +theme[graph_text]={{ $tertiary }} + +# Background color of the percentage meters +theme[meter_bg]={{ $outline }} + +# Misc colors for processes box including mini cpu graphs, details memory graph and details status text +theme[proc_misc]={{ $tertiary }} + +# CPU, Memory, Network, Proc box outline colors +theme[cpu_box]={{ $mauve }} +theme[mem_box]={{ $green }} +theme[net_box]={{ $maroon }} +theme[proc_box]={{ $blue }} + +# Box divider line and small boxes line color +theme[div_line]={{ $outlineVariant }} + +# Temperature graph color (Green -> Yellow -> Red) +theme[temp_start]={{ $green }} +theme[temp_mid]={{ $yellow }} +theme[temp_end]={{ $red }} + +# CPU graph colors (Teal -> Sapphire -> Lavender) +theme[cpu_start]={{ $teal }} +theme[cpu_mid]={{ $sapphire }} +theme[cpu_end]={{ $lavender }} + +# Mem/Disk free meter (Mauve -> Lavender -> Blue) +theme[free_start]={{ $mauve }} +theme[free_mid]={{ $lavender }} +theme[free_end]={{ $blue }} + +# Mem/Disk cached meter (Sapphire -> Blue -> Lavender) +theme[cached_start]={{ $sapphire }} +theme[cached_mid]={{ $blue }} +theme[cached_end]={{ $lavender }} + +# Mem/Disk available meter (Peach -> Maroon -> Red) +theme[available_start]={{ $peach }} +theme[available_mid]={{ $maroon }} +theme[available_end]={{ $red }} + +# Mem/Disk used meter (Green -> Teal -> Sky) +theme[used_start]={{ $green }} +theme[used_mid]={{ $teal }} +theme[used_end]={{ $sky }} + +# Download graph colors (Peach -> Maroon -> Red) +theme[download_start]={{ $peach }} +theme[download_mid]={{ $maroon }} +theme[download_end]={{ $red }} + +# Upload graph colors (Green -> Teal -> Sky) +theme[upload_start]={{ $green }} +theme[upload_mid]={{ $teal }} +theme[upload_end]={{ $sky }} + +# Process box color gradient for threads, mem and cpu usage (Sapphire -> Lavender -> Mauve) +theme[process_start]={{ $sapphire }} +theme[process_mid]={{ $lavender }} +theme[process_end]={{ $mauve }} diff --git a/src/caelestia/data/templates/discord.scss b/src/caelestia/data/templates/discord.scss new file mode 100644 index 0000000..34220d5 --- /dev/null +++ b/src/caelestia/data/templates/discord.scss @@ -0,0 +1,174 @@ +/** + * @name Midnight (Caelestia) + * @description A dark, rounded discord theme. Caelestia scheme colours. + * @author refact0r, esme, anubis + * @version 1.6.2 + * @invite nz87hXyvcy + * @website https://github.com/refact0r/midnight-discord + * @authorId 508863359777505290 + * @authorLink https://www.refact0r.dev +*/ + +@use "sass:color"; +@use "colours" as c; + +@import url("https://refact0r.github.io/midnight-discord/build/midnight.css"); + +body { + /* font, change to '' for default discord font */ + --font: "figtree"; + + /* sizes */ + --gap: 12px; /* spacing between panels */ + --divider-thickness: 4px; /* thickness of unread messages divider and highlighted message borders */ + --border-thickness: 1px; /* thickness of borders around main panels. DOES NOT AFFECT OTHER BORDERS */ + + /* animation/transition options */ + --animations: on; /* turn off to disable all midnight animations/transitions */ + --list-item-transition: 0.2s ease; /* transition for list items */ + --dms-icon-svg-transition: 0.4s ease; /* transition for the dms icon */ + + /* top bar options */ + --top-bar-height: var( + --gap + ); /* height of the titlebar/top bar (discord default is 36px, 24px recommended if moving/hiding top bar buttons) */ + --top-bar-button-position: hide; /* off: default position, hide: hide inbox/support buttons completely, serverlist: move inbox button to server list, titlebar: move inbox button to titlebar (will hide title) */ + --top-bar-title-position: hide; /* off: default centered position, hide: hide title completely, left: left align title (like old discord) */ + --subtle-top-bar-title: off; /* off: default, on: hide the icon and use subtle text color (like old discord) */ + + /* window controls */ + --custom-window-controls: on; /* turn off to use discord default window controls */ + --window-control-size: 14px; /* size of custom window controls */ + + /* dms button icon options */ + --custom-dms-icon: custom; /* off: use default discord icon, hide: remove icon entirely, custom: use custom icon */ + --dms-icon-svg-url: url("https://upload.wikimedia.org/wikipedia/commons/c/c4/Font_Awesome_5_solid_moon.svg"); /* icon svg url. MUST BE A SVG. */ + --dms-icon-svg-size: 90%; /* size of the svg (css mask-size) */ + --dms-icon-color-before: var(--icon-secondary); /* normal icon color */ + --dms-icon-color-after: var(--white); /* icon color when button is hovered/selected */ + + /* dms button background options */ + --custom-dms-background: off; /* off to disable, image to use a background image (must set url variable below), color to use a custom color/gradient */ + --dms-background-image-url: url(""); /* url of the background image */ + --dms-background-image-size: cover; /* size of the background image (css background-size) */ + --dms-background-color: linear-gradient( + 70deg, + var(--blue-2), + var(--purple-2), + var(--red-2) + ); /* fixed color/gradient (css background) */ + + /* background image options */ + --background-image: off; /* turn on to use a background image */ + --background-image-url: url(""); /* url of the background image */ + + /* transparency/blur options */ + /* NOTE: TO USE TRANSPARENCY/BLUR, YOU MUST HAVE TRANSPARENT BG COLORS. FOR EXAMPLE: --bg-4: hsla(220, 15%, 10%, 0.7); */ + --transparency-tweaks: off; /* turn on to remove some elements for better transparency */ + --remove-bg-layer: off; /* turn on to remove the base --bg-3 layer for use with window transparency (WILL OVERRIDE BACKGROUND IMAGE) */ + --panel-blur: off; /* turn on to blur the background of panels */ + --blur-amount: 12px; /* amount of blur */ + --bg-floating: #{c.$surface}; /* you can set this to a more opaque color if floating panels look too transparent */ + + /* chatbar options */ + --custom-chatbar: aligned; /* off: default chatbar, aligned: chatbar aligned with the user panel, separated: chatbar separated from chat */ + --chatbar-height: 47px; /* height of the chatbar (52px by default, 47px recommended for aligned, 56px recommended for separated) */ + --chatbar-padding: 8px; /* padding of the chatbar. only applies in aligned mode. */ + + /* other options */ + --small-user-panel: off; /* turn on to make the user panel smaller like in old discord */ +} + +/* color options */ +:root { + --colors: on; /* turn off to use discord default colors */ + + /* text colors */ + --text-0: #{c.$onPrimary}; /* text on colored elements */ + --text-1: #{color.scale(c.$onSurface, $lightness: 10%)}; /* bright text on colored elements */ + --text-2: #{color.scale(c.$onSurface, $lightness: 5%)}; /* headings and important text */ + --text-3: #{c.$onSurface}; /* normal text */ + --text-4: #{c.$outline}; /* icon buttons and channels */ + --text-5: #{c.$outline}; /* muted channels/chats and timestamps */ + + /* background and dark colors */ + --bg-1: #{c.$surfaceContainerHighest}; /* dark buttons when clicked */ + --bg-2: #{c.$surfaceContainerHigh}; /* dark buttons */ + --bg-3: #{c.$surface}; /* spacing, secondary elements */ + --bg-4: #{c.$surfaceContainer}; /* main background color */ + --hover: #{color.change(c.$onSurface, $alpha: 0.08)}; /* channels and buttons when hovered */ + --active: #{color.change(c.$onSurface, $alpha: 0.1)}; /* channels and buttons when clicked or selected */ + --active-2: #{color.change(c.$onSurface, $alpha: 0.2)}; /* extra state for transparent buttons */ + --message-hover: #{color.change(c.$onSurface, $alpha: 0.08)}; /* messages when hovered */ + + /* accent colors */ + --accent-1: var(--blue-1); /* links and other accent text */ + --accent-2: var(--blue-2); /* small accent elements */ + --accent-3: var(--blue-3); /* accent buttons */ + --accent-4: var(--blue-4); /* accent buttons when hovered */ + --accent-5: var(--blue-5); /* accent buttons when clicked */ + --accent-new: #{c.$error}; /* stuff that's normally red like mute/deafen buttons */ + --mention: linear-gradient( + to right, + color-mix(in hsl, var(--blue-2), transparent 90%) 40%, + transparent + ); /* background of messages that mention you */ + --mention-hover: linear-gradient( + to right, + color-mix(in hsl, var(--blue-2), transparent 95%) 40%, + transparent + ); /* background of messages that mention you when hovered */ + --reply: linear-gradient( + to right, + color-mix(in hsl, var(--text-3), transparent 90%) 40%, + transparent + ); /* background of messages that reply to you */ + --reply-hover: linear-gradient( + to right, + color-mix(in hsl, var(--text-3), transparent 95%) 40%, + transparent + ); /* background of messages that reply to you when hovered */ + + /* status indicator colors */ + --online: var(--green-2); /* change to #43a25a for default */ + --dnd: var(--red-2); /* change to #d83a42 for default */ + --idle: var(--yellow-2); /* change to #ca9654 for default */ + --streaming: var(--purple-2); /* change to #593695 for default */ + --offline: var(--text-4); /* change to #83838b for default offline color */ + + /* border colors */ + --border-light: #{color.change(c.$outline, $alpha: 0)}; /* light border color */ + --border: #{color.change(c.$outline, $alpha: 0)}; /* normal border color */ + --button-border: #{color.change(c.$outline, $alpha: 0)}; /* neutral border color of buttons */ + + /* base colors */ + --red-1: #{c.$error}; + --red-2: #{color.scale(c.$error, $lightness: -5%)}; + --red-3: #{color.scale(c.$error, $lightness: -10%)}; + --red-4: #{color.scale(c.$error, $lightness: -15%)}; + --red-5: #{color.scale(c.$error, $lightness: -20%)}; + + --green-1: #{c.$green}; + --green-2: #{color.scale(c.$green, $lightness: -5%)}; + --green-3: #{color.scale(c.$green, $lightness: -10%)}; + --green-4: #{color.scale(c.$green, $lightness: -15%)}; + --green-5: #{color.scale(c.$green, $lightness: -20%)}; + + --blue-1: #{c.$primary}; + --blue-2: #{color.scale(c.$primary, $lightness: -5%)}; + --blue-3: #{color.scale(c.$primary, $lightness: -10%)}; + --blue-4: #{color.scale(c.$primary, $lightness: -15%)}; + --blue-5: #{color.scale(c.$primary, $lightness: -20%)}; + + --yellow-1: #{c.$yellow}; + --yellow-2: #{color.scale(c.$yellow, $lightness: -5%)}; + --yellow-3: #{color.scale(c.$yellow, $lightness: -10%)}; + --yellow-4: #{color.scale(c.$yellow, $lightness: -15%)}; + --yellow-5: #{color.scale(c.$yellow, $lightness: -20%)}; + + --purple-1: #{c.$mauve}; + --purple-2: #{color.scale(c.$mauve, $lightness: -5%)}; + --purple-3: #{color.scale(c.$mauve, $lightness: -10%)}; + --purple-4: #{color.scale(c.$mauve, $lightness: -15%)}; + --purple-5: #{color.scale(c.$mauve, $lightness: -20%)}; +} diff --git a/src/caelestia/data/templates/fuzzel.ini b/src/caelestia/data/templates/fuzzel.ini new file mode 100644 index 0000000..d61f208 --- /dev/null +++ b/src/caelestia/data/templates/fuzzel.ini @@ -0,0 +1,41 @@ +font=JetBrains Mono NF:size=17 +terminal=foot -e +prompt="> " +layer=overlay +lines=15 +width=60 +dpi-aware=no +inner-pad=10 +horizontal-pad=40 +vertical-pad=15 +match-counter=yes + +[colors] +background=282c34dd +text=abb2bfdd +prompt=d19a66ff +placeholder=666e7cff +input=abb2bfff +match=be5046ff +selection=d19a6687 +selection-text=abb2bfff +selection-match=be5046ff +counter=666e7cff +border=d19a6677 + +[border] +radius=10 +width=2 + +[colors] +background={{ $surface }}dd +text={{ $onSurface }}dd +prompt={{ $primary }}ff +placeholder={{ $outline }}ff +input={{ $onSurface }}ff +match={{ $tertiary }}ff +selection={{ $primary }}87 +selection-text={{ $onSurface }}ff +selection-match={{ $tertiary }}ff +counter={{ $outline }}ff +border={{ $primary }}77 diff --git a/src/caelestia/data/templates/spicetify-dark.ini b/src/caelestia/data/templates/spicetify-dark.ini new file mode 100644 index 0000000..4bf85eb --- /dev/null +++ b/src/caelestia/data/templates/spicetify-dark.ini @@ -0,0 +1,19 @@ +[caelestia] +text = {{ $onSurface }} ; Main text colour +subtext = {{ $onSurfaceVariant }} ; Subtext colour +main = {{ $surfaceContainer }} ; Panel backgrounds +highlight = {{ $primary }} ; Doesn't seem to do anything +misc = {{ $primary }} ; Doesn't seem to do anything +notification = {{ $outline }} ; Notifications probably +notification-error = {{ $error }} ; Error notifications probably +shadow = {{ $shadow }} ; Shadow for covers, context menu, also affects playlist/artist banners +card = {{ $surfaceContainerHigh }} ; Context menu and tooltips +player = {{ $secondaryContainer }} ; Background for top result in search +sidebar = {{ $surface }} ; Background +main-elevated = {{ $surfaceContainerHigh }} ; Higher layers than main, e.g. search bar +highlight-elevated = {{ $surfaceContainerHighest }} ; Home button and search bar accelerator +selected-row = {{ $onSurface }} ; Selections, hover, other coloured text and slider background +button = {{ $primary }} ; Slider and text buttons +button-active = {{ $primary }} ; Background buttons +button-disabled = {{ $outline }} ; Disabled buttons +tab-active = {{ $surfaceContainerHigh }} ; Profile fallbacks in search diff --git a/src/caelestia/data/templates/spicetify-light.ini b/src/caelestia/data/templates/spicetify-light.ini new file mode 100644 index 0000000..a8b361b --- /dev/null +++ b/src/caelestia/data/templates/spicetify-light.ini @@ -0,0 +1,19 @@ +[caelestia] +text = {{ $onSurface }} ; Main text colour +subtext = {{ $onSurfaceVariant }} ; Subtext colour +main = {{ $surface }} ; Panel backgrounds +highlight = {{ $primary }} ; Doesn't seem to do anything +misc = {{ $primary }} ; Doesn't seem to do anything +notification = {{ $outline }} ; Notifications probably +notification-error = {{ $error }} ; Error notifications probably +shadow = {{ $shadow }} ; Shadow for covers, context menu, also affects playlist/artist banners +card = {{ $surfaceContainer }} ; Context menu and tooltips +player = {{ $secondaryContainer }} ; Background for top result in search +sidebar = {{ $surfaceContainer }} ; Background +main-elevated = {{ $surfaceContainerHigh }} ; Higher layers than main, e.g. search bar +highlight-elevated = {{ $surfaceContainerHighest }} ; Home button and search bar accelerator +selected-row = {{ $onSurface }} ; Selections, hover, other coloured text and slider background +button = {{ $primary }} ; Slider and text buttons +button-active = {{ $primary }} ; Background buttons +button-disabled = {{ $outline }} ; Disabled buttons +tab-active = {{ $surfaceContainer }} ; Profile fallbacks in search diff --git a/src/caelestia/parser.py b/src/caelestia/parser.py new file mode 100644 index 0000000..6d0b552 --- /dev/null +++ b/src/caelestia/parser.py @@ -0,0 +1,112 @@ +import argparse + +from caelestia.subcommands import clipboard, emoji, pip, record, scheme, screenshot, shell, toggle, wallpaper, wsaction +from caelestia.utils.paths import wallpapers_dir +from caelestia.utils.scheme import get_scheme_names, scheme_variants +from caelestia.utils.wallpaper import get_wallpaper + + +def parse_args() -> (argparse.ArgumentParser, argparse.Namespace): + parser = argparse.ArgumentParser(prog="caelestia", description="Main control script for the Caelestia dotfiles") + + # Add subcommand parsers + command_parser = parser.add_subparsers( + title="subcommands", description="valid subcommands", metavar="COMMAND", help="the subcommand to run" + ) + + # Create parser for shell opts + shell_parser = command_parser.add_parser("shell", help="start or message the shell") + shell_parser.set_defaults(cls=shell.Command) + shell_parser.add_argument("message", nargs="*", help="a message to send to the shell") + shell_parser.add_argument("-s", "--show", action="store_true", help="print all shell IPC commands") + shell_parser.add_argument( + "-l", + "--log", + nargs="?", + const="quickshell.dbus.properties.warning=false;quickshell.dbus.dbusmenu.warning=false;quickshell.service.notifications.warning=false;quickshell.service.sni.host.warning=false", + metavar="RULES", + help="print the shell log", + ) + + # Create parser for toggle opts + toggle_parser = command_parser.add_parser("toggle", help="toggle a special workspace") + toggle_parser.set_defaults(cls=toggle.Command) + toggle_parser.add_argument( + "workspace", choices=["communication", "music", "sysmon", "specialws", "todo"], help="the workspace to toggle" + ) + + # Create parser for workspace-action opts + ws_action_parser = command_parser.add_parser( + "workspace-action", help="execute a Hyprland workspace dispatcher in the current group" + ) + ws_action_parser.set_defaults(cls=wsaction.Command) + ws_action_parser.add_argument( + "-g", "--group", action="store_true", help="whether to execute the dispatcher on a group" + ) + ws_action_parser.add_argument( + "dispatcher", choices=["workspace", "movetoworkspace"], help="the dispatcher to execute" + ) + ws_action_parser.add_argument("workspace", type=int, help="the workspace to pass to the dispatcher") + + # Create parser for scheme opts + scheme_parser = command_parser.add_parser("scheme", help="manage the colour scheme") + scheme_parser.set_defaults(cls=scheme.Command) + scheme_parser.add_argument("-r", "--random", action="store_true", help="switch to a random scheme") + scheme_parser.add_argument("-n", "--name", choices=get_scheme_names(), help="the name of the scheme to switch to") + scheme_parser.add_argument("-f", "--flavour", help="the flavour to switch to") + scheme_parser.add_argument("-m", "--mode", choices=["dark", "light"], help="the mode to switch to") + scheme_parser.add_argument("-v", "--variant", choices=scheme_variants, help="the variant to switch to") + + # Create parser for screenshot opts + screenshot_parser = command_parser.add_parser("screenshot", help="take a screenshot") + screenshot_parser.set_defaults(cls=screenshot.Command) + screenshot_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="take a screenshot of a region") + screenshot_parser.add_argument( + "-f", "--freeze", action="store_true", help="freeze the screen while selecting a region" + ) + + # Create parser for record opts + record_parser = command_parser.add_parser("record", help="start a screen recording") + record_parser.set_defaults(cls=record.Command) + record_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="record a region") + record_parser.add_argument("-s", "--sound", action="store_true", help="record audio") + + # Create parser for clipboard opts + clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history") + clipboard_parser.set_defaults(cls=clipboard.Command) + clipboard_parser.add_argument("-d", "--delete", action="store_true", help="delete from clipboard history") + + # Create parser for emoji-picker opts + emoji_parser = command_parser.add_parser("emoji-picker", help="toggle the emoji picker") + emoji_parser.set_defaults(cls=emoji.Command) + + # Create parser for wallpaper opts + wallpaper_parser = command_parser.add_parser("wallpaper", help="manage the wallpaper") + wallpaper_parser.set_defaults(cls=wallpaper.Command) + wallpaper_parser.add_argument( + "-p", "--print", nargs="?", const=get_wallpaper(), metavar="PATH", help="print the scheme for a wallpaper" + ) + wallpaper_parser.add_argument( + "-r", "--random", nargs="?", const=wallpapers_dir, metavar="DIR", help="switch to a random wallpaper" + ) + wallpaper_parser.add_argument("-f", "--file", help="the path to the wallpaper to switch to") + wallpaper_parser.add_argument("-n", "--no-filter", action="store_true", help="do not filter by size") + wallpaper_parser.add_argument( + "-t", + "--threshold", + default=0.8, + help="the minimum percentage of the largest monitor size the image must be greater than to be selected", + ) + wallpaper_parser.add_argument( + "-N", + "--no-smart", + action="store_true", + help="do not automatically change the scheme mode based on wallpaper colour", + ) + + # Create parser for pip opts + pip_parser = command_parser.add_parser("pip", help="picture in picture utilities") + pip_parser.set_defaults(cls=pip.Command) + pip_parser.add_argument("-d", "--daemon", action="store_true", help="start the daemon") + + return parser, parser.parse_args() diff --git a/src/caelestia/subcommands/clipboard.py b/src/caelestia/subcommands/clipboard.py new file mode 100644 index 0000000..c0eddb5 --- /dev/null +++ b/src/caelestia/subcommands/clipboard.py @@ -0,0 +1,25 @@ +import subprocess +from argparse import Namespace + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + clip = subprocess.check_output(["cliphist", "list"]) + + if self.args.delete: + args = ["--prompt=del > ", "--placeholder=Delete from clipboard"] + else: + args = ["--placeholder=Type to search clipboard"] + + chosen = subprocess.check_output(["fuzzel", "--dmenu", *args], input=clip) + + if self.args.delete: + subprocess.run(["cliphist", "delete"], input=chosen) + else: + decoded = subprocess.check_output(["cliphist", "decode"], input=chosen) + subprocess.run(["wl-copy"], input=decoded) diff --git a/src/caelestia/subcommands/emoji.py b/src/caelestia/subcommands/emoji.py new file mode 100644 index 0000000..f04b502 --- /dev/null +++ b/src/caelestia/subcommands/emoji.py @@ -0,0 +1,18 @@ +import subprocess +from argparse import Namespace + +from caelestia.utils.paths import cli_data_dir + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + emojis = (cli_data_dir / "emojis.txt").read_text() + chosen = subprocess.check_output( + ["fuzzel", "--dmenu", "--placeholder=Type to search emojis"], input=emojis, text=True + ) + subprocess.run(["wl-copy"], input=chosen.split()[0], text=True) diff --git a/src/caelestia/subcommands/pip.py b/src/caelestia/subcommands/pip.py new file mode 100644 index 0000000..5f1b5fa --- /dev/null +++ b/src/caelestia/subcommands/pip.py @@ -0,0 +1,44 @@ +import re +import socket +from argparse import Namespace + +from caelestia.utils import hypr + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + if self.args.daemon: + self.daemon() + else: + win = hypr.message("activewindow") + if win["floating"]: + self.handle_window(win["address"], win["workspace"]["name"]) + + def handle_window(self, address: str, ws: str) -> None: + mon_id = next(w for w in hypr.message("workspaces") if w["name"] == ws)["monitorID"] + mon = next(m for m in hypr.message("monitors") if m["id"] == mon_id) + width, height = next(c for c in hypr.message("clients") if c["address"] == address)["size"] + + scale_factor = mon["height"] / 4 / height + scaled_win_size = f"{int(width * scale_factor)} {int(height * scale_factor)}" + off = min(mon["width"], mon["height"]) * 0.03 + move_to = f"{int(mon['width'] - off - width * scale_factor)} {int(mon['height'] - off - height * scale_factor)}" + + hypr.dispatch("resizewindowpixel", "exact", f"{scaled_win_size},address:{address}") + hypr.dispatch("movewindowpixel", "exact", f"{move_to},address:{address}") + + def daemon(self) -> None: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(hypr.socket2_path) + + while True: + data = sock.recv(4096).decode() + if data.startswith("openwindow>>"): + address, ws, cls, title = data[12:].split(",") + if re.match(r"^[Pp]icture(-| )in(-| )[Pp]icture$", title): + self.handle_window(f"0x{address}", ws) diff --git a/src/caelestia/subcommands/record.py b/src/caelestia/subcommands/record.py new file mode 100644 index 0000000..a4fa51d --- /dev/null +++ b/src/caelestia/subcommands/record.py @@ -0,0 +1,122 @@ +import subprocess +import time +from argparse import Namespace +from datetime import datetime + +from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + proc = subprocess.run(["pidof", "wl-screenrec"]) + if proc.returncode == 0: + self.stop() + else: + self.start() + + def start(self) -> None: + args = [] + + if self.args.region: + if self.args.region == "slurp": + region = subprocess.check_output(["slurp"], text=True) + else: + region = self.args.region + args += ["-g", region.strip()] + + if self.args.sound: + sources = subprocess.check_output(["pactl", "list", "short", "sources"], text=True).splitlines() + for source in sources: + if "RUNNING" in source: + args += ["--audio", "--audio-device", source.split()[1]] + break + else: + raise ValueError("No audio source found") + + proc = subprocess.Popen( + ["wl-screenrec", *args, "--codec", "hevc", "-f", recording_path], + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) + + # Send notif if proc hasn't ended after a small delay + time.sleep(0.1) + if proc.poll() is None: + notif = subprocess.check_output( + ["notify-send", "-p", "-a", "caelestia-cli", "Recording started", "Recording..."], text=True + ).strip() + recording_notif_path.write_text(notif) + else: + subprocess.run( + [ + "notify-send", + "-a", + "caelestia-cli", + "Recording failed", + f"Recording failed to start: {proc.communicate()[1]}", + ] + ) + + def stop(self) -> None: + subprocess.run(["pkill", "wl-screenrec"]) + + # Move to recordings folder + new_path = recordings_dir / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4" + recording_path.rename(new_path) + + # Close start notification + try: + notif = recording_notif_path.read_text() + subprocess.run( + [ + "gdbus", + "call", + "--session", + "--dest=org.freedesktop.Notifications", + "--object-path=/org/freedesktop/Notifications", + "--method=org.freedesktop.Notifications.CloseNotification", + notif, + ] + ) + except IOError: + pass + + action = subprocess.check_output( + [ + "notify-send", + "-a", + "caelestia-cli", + "--action=watch=Watch", + "--action=open=Open", + "--action=delete=Delete", + "Recording stopped", + f"Recording saved in {new_path}", + ], + text=True, + ).strip() + + if action == "watch": + subprocess.Popen(["app2unit", "-O", new_path], start_new_session=True) + elif action == "open": + p = subprocess.run( + [ + "dbus-send", + "--session", + "--dest=org.freedesktop.FileManager1", + "--type=method_call", + "/org/freedesktop/FileManager1", + "org.freedesktop.FileManager1.ShowItems", + f"array:string:file://{new_path}", + "string:", + ] + ) + if p.returncode != 0: + subprocess.Popen(["app2unit", "-O", new_path.parent], start_new_session=True) + elif action == "delete": + new_path.unlink() diff --git a/src/caelestia/subcommands/scheme.py b/src/caelestia/subcommands/scheme.py new file mode 100644 index 0000000..c95df96 --- /dev/null +++ b/src/caelestia/subcommands/scheme.py @@ -0,0 +1,30 @@ +from argparse import Namespace + +from caelestia.utils.scheme import get_scheme +from caelestia.utils.theme import apply_colours + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + scheme = get_scheme() + + if self.args.random: + scheme.set_random() + apply_colours(scheme.colours, scheme.mode) + elif self.args.name or self.args.flavour or self.args.mode or self.args.variant: + if self.args.name: + scheme.name = self.args.name + if self.args.flavour: + scheme.flavour = self.args.flavour + if self.args.mode: + scheme.mode = self.args.mode + if self.args.variant: + scheme.variant = self.args.variant + apply_colours(scheme.colours, scheme.mode) + else: + print(scheme) diff --git a/src/caelestia/subcommands/screenshot.py b/src/caelestia/subcommands/screenshot.py new file mode 100644 index 0000000..73d65f7 --- /dev/null +++ b/src/caelestia/subcommands/screenshot.py @@ -0,0 +1,78 @@ +import subprocess +from argparse import Namespace +from datetime import datetime + +from caelestia.utils import hypr +from caelestia.utils.paths import screenshots_cache_dir, screenshots_dir + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + if self.args.region: + self.region() + else: + self.fullscreen() + + def region(self) -> None: + freeze_proc = None + + if self.args.freeze: + freeze_proc = subprocess.Popen(["wayfreeze", "--hide-cursor"]) + + if self.args.region == "slurp": + ws = hypr.message("activeworkspace")["id"] + geoms = [ + f"{','.join(map(str, c['at']))} {'x'.join(map(str, c['size']))}" + for c in hypr.message("clients") + if c["workspace"]["id"] == ws + ] + region = subprocess.check_output(["slurp"], input="\n".join(geoms), text=True) + else: + region = self.args.region + + sc_data = subprocess.check_output(["grim", "-l", "0", "-g", region.strip(), "-"]) + swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True) + swappy.stdin.write(sc_data) + swappy.stdin.close() + + if freeze_proc: + freeze_proc.kill() + + def fullscreen(self) -> None: + sc_data = subprocess.check_output(["grim", "-"]) + + subprocess.run(["wl-copy"], input=sc_data) + + dest = screenshots_cache_dir / datetime.now().strftime("%Y%m%d%H%M%S") + screenshots_cache_dir.mkdir(exist_ok=True, parents=True) + dest.write_bytes(sc_data) + + action = subprocess.check_output( + [ + "notify-send", + "-i", + "image-x-generic-symbolic", + "-h", + f"STRING:image-path:{dest}", + "-a", + "caelestia-cli", + "--action=open=Open", + "--action=save=Save", + "Screenshot taken", + f"Screenshot stored in {dest} and copied to clipboard", + ], + text=True, + ).strip() + + if action == "open": + subprocess.Popen(["swappy", "-f", dest], start_new_session=True) + elif action == "save": + new_dest = (screenshots_dir / dest.name).with_suffix(".png") + new_dest.parent.mkdir(exist_ok=True, parents=True) + dest.rename(new_dest) + subprocess.run(["notify-send", "Screenshot saved", f"Saved to {new_dest}"]) diff --git a/src/caelestia/subcommands/shell.py b/src/caelestia/subcommands/shell.py new file mode 100644 index 0000000..25a39d8 --- /dev/null +++ b/src/caelestia/subcommands/shell.py @@ -0,0 +1,41 @@ +import subprocess +from argparse import Namespace + +from caelestia.utils import paths + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + if self.args.show: + # Print the ipc + self.print_ipc() + elif self.args.log: + # Print the log + self.print_log() + elif self.args.message: + # Send a message + self.message(*self.args.message) + else: + # Start the shell + self.shell() + + def shell(self, *args: list[str]) -> str: + return subprocess.check_output(["qs", "-p", paths.c_data_dir / "shell", *args], text=True) + + def print_ipc(self) -> None: + print(self.shell("ipc", "show"), end="") + + def print_log(self) -> None: + log = self.shell("log") + # FIXME: remove when logging rules are added/warning is removed + for line in log.splitlines(): + if "QProcess: Destroyed while process" not in line: + print(line) + + def message(self, *args: list[str]) -> None: + print(self.shell("ipc", "call", *args), end="") diff --git a/src/caelestia/subcommands/toggle.py b/src/caelestia/subcommands/toggle.py new file mode 100644 index 0000000..b8ad11b --- /dev/null +++ b/src/caelestia/subcommands/toggle.py @@ -0,0 +1,75 @@ +import subprocess +from argparse import Namespace + +from caelestia.utils import hypr + + +class Command: + args: Namespace + clients: list[dict[str, any]] = None + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + getattr(self, self.args.workspace)() + + def get_clients(self) -> list[dict[str, any]]: + if self.clients is None: + self.clients = hypr.message("clients") + + return self.clients + + def move_client(self, selector: callable, workspace: str) -> None: + for client in self.get_clients(): + if selector(client): + hypr.dispatch("movetoworkspacesilent", f"special:{workspace},address:{client['address']}") + + def spawn_client(self, selector: callable, spawn: list[str]) -> bool: + exists = any(selector(client) for client in self.get_clients()) + + if not exists: + subprocess.Popen(["app2unit", "--", *spawn], start_new_session=True) + + return not exists + + def spawn_or_move(self, selector: callable, spawn: list[str], workspace: str) -> None: + if not self.spawn_client(selector, spawn): + self.move_client(selector, workspace) + + def communication(self) -> None: + self.spawn_or_move(lambda c: c["class"] == "discord", ["discord"], "communication") + self.move_client(lambda c: c["class"] == "whatsapp", "communication") + hypr.dispatch("togglespecialworkspace", "communication") + + def music(self) -> None: + self.spawn_or_move( + lambda c: c["class"] == "Spotify" or c["initialTitle"] == "Spotify" or c["initialTitle"] == "Spotify Free", + ["spicetify", "watch", "-s"], + "music", + ) + self.move_client(lambda c: c["class"] == "feishin", "music") + hypr.dispatch("togglespecialworkspace", "music") + + def sysmon(self) -> None: + self.spawn_client( + lambda c: c["class"] == "btop" and c["title"] == "btop" and c["workspace"]["name"] == "special:sysmon", + ["foot", "-a", "btop", "-T", "btop", "--", "btop"], + ) + hypr.dispatch("togglespecialworkspace", "sysmon") + + def todo(self) -> None: + self.spawn_or_move(lambda c: c["class"] == "Todoist", ["todoist"], "todo") + hypr.dispatch("togglespecialworkspace", "todo") + + def specialws(self) -> None: + workspaces = hypr.message("workspaces") + on_special_ws = any(ws["name"] == "special:special" for ws in workspaces) + toggle_ws = "special" + + if not on_special_ws: + active_ws = hypr.message("activewindow")["workspace"]["name"] + if active_ws.startswith("special:"): + toggle_ws = active_ws[8:] + + hypr.dispatch("togglespecialworkspace", toggle_ws) diff --git a/src/caelestia/subcommands/wallpaper.py b/src/caelestia/subcommands/wallpaper.py new file mode 100644 index 0000000..940dcb5 --- /dev/null +++ b/src/caelestia/subcommands/wallpaper.py @@ -0,0 +1,21 @@ +import json +from argparse import Namespace + +from caelestia.utils.wallpaper import get_colours_for_wall, get_wallpaper, set_random, set_wallpaper + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + if self.args.print: + print(json.dumps(get_colours_for_wall(self.args.print, self.args.no_smart))) + elif self.args.file: + set_wallpaper(self.args.file, self.args.no_smart) + elif self.args.random: + set_random(self.args) + else: + print(get_wallpaper() or "No wallpaper set") diff --git a/src/caelestia/subcommands/wsaction.py b/src/caelestia/subcommands/wsaction.py new file mode 100644 index 0000000..d496381 --- /dev/null +++ b/src/caelestia/subcommands/wsaction.py @@ -0,0 +1,18 @@ +from argparse import Namespace + +from caelestia.utils import hypr + + +class Command: + args: Namespace + + def __init__(self, args: Namespace) -> None: + self.args = args + + def run(self) -> None: + active_ws = hypr.message("activeworkspace")["id"] + + if self.args.group: + hypr.dispatch(self.args.dispatcher, (self.args.workspace - 1) * 10 + active_ws % 10) + else: + hypr.dispatch(self.args.dispatcher, int((active_ws - 1) / 10) * 10 + self.args.workspace) diff --git a/src/caelestia/utils/hypr.py b/src/caelestia/utils/hypr.py new file mode 100644 index 0000000..f89cd98 --- /dev/null +++ b/src/caelestia/utils/hypr.py @@ -0,0 +1,29 @@ +import json as j +import os +import socket + +socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}" +socket_path = f"{socket_base}/.socket.sock" +socket2_path = f"{socket_base}/.socket2.sock" + + +def message(msg: str, json: bool = True) -> str | dict[str, any]: + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: + sock.connect(socket_path) + + if json: + msg = f"j/{msg}" + sock.send(msg.encode()) + + resp = sock.recv(8192).decode() + while True: + new_resp = sock.recv(8192) + if not new_resp: + break + resp += new_resp.decode() + + return j.loads(resp) if json else resp + + +def dispatch(dispatcher: str, *args: list[any]) -> bool: + return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok" diff --git a/src/caelestia/utils/material/__init__.py b/src/caelestia/utils/material/__init__.py new file mode 100644 index 0000000..8adab1f --- /dev/null +++ b/src/caelestia/utils/material/__init__.py @@ -0,0 +1,52 @@ +import json +from pathlib import Path + +from materialyoucolor.hct import Hct + +from caelestia.utils.material.generator import gen_scheme +from caelestia.utils.material.score import score +from caelestia.utils.paths import compute_hash, scheme_cache_dir, wallpaper_thumbnail_path + + +def get_score_for_image(image: str, cache_base: Path) -> tuple[list[Hct], list[Hct]]: + cache = cache_base / "score.json" + + try: + with cache.open("r") as f: + return [[Hct.from_int(c) for c in li] for li in json.load(f)] + except (IOError, json.JSONDecodeError): + pass + + s = score(image) + + cache.parent.mkdir(parents=True, exist_ok=True) + with cache.open("w") as f: + json.dump([[c.to_int() for c in li] for li in s], f) + + return s + + +def get_colours_for_image(image: str = str(wallpaper_thumbnail_path), scheme=None) -> dict[str, str]: + if scheme is None: + from caelestia.utils.scheme import get_scheme + + scheme = get_scheme() + + cache_base = scheme_cache_dir / compute_hash(image) + cache = (cache_base / scheme.variant / scheme.flavour / scheme.mode).with_suffix(".json") + + try: + with cache.open("r") as f: + return json.load(f) + except (IOError, json.JSONDecodeError): + pass + + primaries, colours = get_score_for_image(image, cache_base) + i = ["default", "alt1", "alt2"].index(scheme.flavour) + scheme = gen_scheme(scheme, primaries[i], colours) + + cache.parent.mkdir(parents=True, exist_ok=True) + with cache.open("w") as f: + json.dump(scheme, f) + + return scheme diff --git a/src/caelestia/utils/material/generator.py b/src/caelestia/utils/material/generator.py new file mode 100644 index 0000000..584d375 --- /dev/null +++ b/src/caelestia/utils/material/generator.py @@ -0,0 +1,194 @@ +from materialyoucolor.blend import Blend +from materialyoucolor.dynamiccolor.material_dynamic_colors import ( + DynamicScheme, + MaterialDynamicColors, +) +from materialyoucolor.hct import Hct +from materialyoucolor.hct.cam16 import Cam16 +from materialyoucolor.scheme.scheme_content import SchemeContent +from materialyoucolor.scheme.scheme_expressive import SchemeExpressive +from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity +from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad +from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome +from materialyoucolor.scheme.scheme_neutral import SchemeNeutral +from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow +from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot +from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant + + +def hex_to_hct(hex: str) -> Hct: + return Hct.from_int(int(f"0xFF{hex}", 16)) + + +light_colours = [ + hex_to_hct("dc8a78"), + hex_to_hct("dd7878"), + hex_to_hct("ea76cb"), + hex_to_hct("8839ef"), + hex_to_hct("d20f39"), + hex_to_hct("e64553"), + hex_to_hct("fe640b"), + hex_to_hct("df8e1d"), + hex_to_hct("40a02b"), + hex_to_hct("179299"), + hex_to_hct("04a5e5"), + hex_to_hct("209fb5"), + hex_to_hct("1e66f5"), + hex_to_hct("7287fd"), +] + +dark_colours = [ + hex_to_hct("f5e0dc"), + hex_to_hct("f2cdcd"), + hex_to_hct("f5c2e7"), + hex_to_hct("cba6f7"), + hex_to_hct("f38ba8"), + hex_to_hct("eba0ac"), + hex_to_hct("fab387"), + hex_to_hct("f9e2af"), + hex_to_hct("a6e3a1"), + hex_to_hct("94e2d5"), + hex_to_hct("89dceb"), + hex_to_hct("74c7ec"), + hex_to_hct("89b4fa"), + hex_to_hct("b4befe"), +] + +colour_names = [ + "rosewater", + "flamingo", + "pink", + "mauve", + "red", + "maroon", + "peach", + "yellow", + "green", + "teal", + "sky", + "sapphire", + "blue", + "lavender", + "success", + "error", +] + + +def grayscale(colour: Hct, light: bool) -> Hct: + colour = darken(colour, 0.35) if light else lighten(colour, 0.65) + colour.chroma = 0 + return colour + + +def mix(a: Hct, b: Hct, w: float) -> Hct: + return Hct.from_int(Blend.cam16_ucs(a.to_int(), b.to_int(), w)) + + +def harmonize(a: Hct, b: Hct) -> Hct: + return Hct.from_int(Blend.harmonize(a.to_int(), b.to_int())) + + +def lighten(colour: Hct, amount: float) -> Hct: + diff = (100 - colour.tone) * amount + return Hct.from_hct(colour.hue, colour.chroma + diff / 5, colour.tone + diff) + + +def darken(colour: Hct, amount: float) -> Hct: + diff = colour.tone * amount + return Hct.from_hct(colour.hue, colour.chroma + diff / 5, colour.tone - diff) + + +def distance(colour: Cam16, base: Cam16) -> float: + return colour.distance(base) + + +def smart_sort(colours: list[Hct], base: list[Hct]) -> dict[str, Hct]: + sorted_colours = [None] * len(colours) + distances = {} + + cams = [(c, Cam16.from_int(c.to_int())) for c in colours] + base_cams = [Cam16.from_int(c.to_int()) for c in base] + + for colour, cam in cams: + dist = [(i, distance(cam, b)) for i, b in enumerate(base_cams)] + dist.sort(key=lambda x: x[1]) + distances[colour] = dist + + for colour in colours: + while len(distances[colour]) > 0: + i, dist = distances[colour][0] + + if sorted_colours[i] is None: + sorted_colours[i] = colour, dist + break + elif sorted_colours[i][1] > dist: + old = sorted_colours[i][0] + sorted_colours[i] = colour, dist + colour = old + + distances[colour].pop(0) + + return {colour_names[i]: c[0] for i, c in enumerate(sorted_colours)} + + +def get_scheme(scheme: str) -> DynamicScheme: + if scheme == "content": + return SchemeContent + if scheme == "expressive": + return SchemeExpressive + if scheme == "fidelity": + return SchemeFidelity + if scheme == "fruitsalad": + return SchemeFruitSalad + if scheme == "monochrome": + return SchemeMonochrome + if scheme == "neutral": + return SchemeNeutral + if scheme == "rainbow": + return SchemeRainbow + if scheme == "tonalspot": + return SchemeTonalSpot + return SchemeVibrant + + +def gen_scheme(scheme, primary: Hct, colours: list[Hct]) -> dict[str, str]: + light = scheme.mode == "light" + base = light_colours if light else dark_colours + + # Sort colours and turn into dict + colours = smart_sort(colours, base) + + # Harmonize colours + for name, hct in colours.items(): + if scheme.variant == "monochrome": + colours[name] = grayscale(hct, light) + else: + harmonized = harmonize(hct, primary) + colours[name] = darken(harmonized, 0.35) if light else lighten(harmonized, 0.65) + + # Material colours + primary_scheme = get_scheme(scheme.variant)(primary, not light, 0) + for colour in vars(MaterialDynamicColors).keys(): + colour_name = getattr(MaterialDynamicColors, colour) + if hasattr(colour_name, "get_hct"): + colours[colour] = colour_name.get_hct(primary_scheme) + + # FIXME: deprecated stuff + colours["text"] = colours["onBackground"] + colours["subtext1"] = colours["onSurfaceVariant"] + colours["subtext0"] = colours["outline"] + colours["overlay2"] = mix(colours["surface"], colours["outline"], 0.86) + colours["overlay1"] = mix(colours["surface"], colours["outline"], 0.71) + colours["overlay0"] = mix(colours["surface"], colours["outline"], 0.57) + colours["surface2"] = mix(colours["surface"], colours["outline"], 0.43) + colours["surface1"] = mix(colours["surface"], colours["outline"], 0.29) + colours["surface0"] = mix(colours["surface"], colours["outline"], 0.14) + colours["base"] = colours["surface"] + colours["mantle"] = darken(colours["surface"], 0.03) + colours["crust"] = darken(colours["surface"], 0.05) + colours["success"] = harmonize(base[8], primary) + + # For debugging + # print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()])) + + return {k: hex(v.to_int())[4:] for k, v in colours.items()} diff --git a/scheme/score.py b/src/caelestia/utils/material/score.py index 18ddc21..7765050 100755..100644 --- a/scheme/score.py +++ b/src/caelestia/utils/material/score.py @@ -20,9 +20,8 @@ class Score: pass @staticmethod - def score(colors_to_population: dict) -> tuple[list[Hct], list[Hct]]: + def score(colors_to_population: dict, filter_enabled: bool = True) -> tuple[list[Hct], list[Hct]]: desired = 14 - filter_enabled = False dislike_filter = True colors_hct = [] @@ -50,18 +49,11 @@ class Score: hue = int(sanitize_degrees_int(round(hct.hue))) proportion = hue_excited_proportions[hue] - if filter_enabled and ( - hct.chroma < Score.CUTOFF_CHROMA - or proportion <= Score.CUTOFF_EXCITED_PROPORTION - ): + if filter_enabled and (hct.chroma < Score.CUTOFF_CHROMA or proportion <= Score.CUTOFF_EXCITED_PROPORTION): continue proportion_score = proportion * 100.0 * Score.WEIGHT_PROPORTION - chroma_weight = ( - Score.WEIGHT_CHROMA_BELOW - if hct.chroma < Score.TARGET_CHROMA - else Score.WEIGHT_CHROMA_ABOVE - ) + chroma_weight = Score.WEIGHT_CHROMA_BELOW if hct.chroma < Score.TARGET_CHROMA else Score.WEIGHT_CHROMA_ABOVE chroma_score = (hct.chroma - Score.TARGET_CHROMA) * chroma_weight score = proportion_score + chroma_score scored_hct.append({"hct": hct, "score": score}) @@ -75,8 +67,7 @@ class Score: for item in scored_hct: hct = item["hct"] duplicate_hue = any( - difference_degrees(hct.hue, chosen_hct.hue) < difference_degrees_ - for chosen_hct in chosen_colors + difference_degrees(hct.hue, chosen_hct.hue) < difference_degrees_ for chosen_hct in chosen_colors ) if not duplicate_hue: chosen_colors.append(hct) @@ -102,8 +93,7 @@ class Score: for item in scored_hct: hct = item["hct"] duplicate_hue = any( - difference_degrees(hct.hue, chosen_hct.hue) < difference_degrees_ - for chosen_hct in chosen_primaries + difference_degrees(hct.hue, chosen_hct.hue) < difference_degrees_ for chosen_hct in chosen_primaries ) if not duplicate_hue: chosen_primaries.append(hct) @@ -119,16 +109,24 @@ class Score: for i, chosen_hct in enumerate(chosen_colors): chosen_colors[i] = DislikeAnalyzer.fix_if_disliked(chosen_hct) + # Ensure enough colours + if len(chosen_colors) < desired: + return Score.score(colors_to_population, False) + return chosen_primaries, chosen_colors +def score(image: str) -> tuple[list[Hct], list[Hct]]: + return Score.score(ImageQuantizeCelebi(image, 1, 128)) + + if __name__ == "__main__": img = sys.argv[1] mode = sys.argv[2] if len(sys.argv) > 2 else "hex" colours = Score.score(ImageQuantizeCelebi(img, 1, 128)) - for l in colours: + for t in colours: if mode != "hex": - print("".join(["\x1b[48;2;{};{};{}m \x1b[0m".format(*c.to_rgba()[:3]) for c in l])) + print("".join(["\x1b[48;2;{};{};{}m \x1b[0m".format(*c.to_rgba()[:3]) for c in t])) if mode != "swatch": - print(" ".join(["{:02X}{:02X}{:02X}".format(*c.to_rgba()[:3]) for c in l])) + print(" ".join(["{:02X}{:02X}{:02X}".format(*c.to_rgba()[:3]) for c in t])) diff --git a/src/caelestia/utils/paths.py b/src/caelestia/utils/paths.py new file mode 100644 index 0000000..a4ef36f --- /dev/null +++ b/src/caelestia/utils/paths.py @@ -0,0 +1,53 @@ +import hashlib +import json +import os +import shutil +import tempfile +from pathlib import Path + +config_dir = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) +data_dir = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) +state_dir = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state")) +cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) + +c_config_dir = config_dir / "caelestia" +c_data_dir = data_dir / "caelestia" +c_state_dir = state_dir / "caelestia" +c_cache_dir = cache_dir / "caelestia" + +cli_data_dir = Path(__file__).parent.parent / "data" +templates_dir = cli_data_dir / "templates" + +scheme_path = c_state_dir / "scheme.json" +scheme_data_dir = cli_data_dir / "schemes" +scheme_cache_dir = c_cache_dir / "schemes" + +wallpapers_dir = Path.home() / "Pictures/Wallpapers" +wallpaper_path_path = c_state_dir / "wallpaper/path.txt" +wallpaper_link_path = c_state_dir / "wallpaper/current" +wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg" +wallpapers_cache_dir = c_cache_dir / "wallpapers" + +screenshots_dir = Path.home() / "Pictures/Screenshots" +screenshots_cache_dir = c_cache_dir / "screenshots" + +recordings_dir = Path.home() / "Videos/Recordings" +recording_path = c_state_dir / "record/recording.mp4" +recording_notif_path = c_state_dir / "record/notifid.txt" + + +def compute_hash(path: Path | str) -> str: + sha = hashlib.sha256() + + with open(path, "rb") as f: + while chunk := f.read(8192): + sha.update(chunk) + + return sha.hexdigest() + + +def atomic_dump(path: Path, content: dict[str, any]) -> None: + with tempfile.NamedTemporaryFile("w") as f: + json.dump(content, f) + f.flush() + shutil.move(f.name, path) diff --git a/src/caelestia/utils/scheme.py b/src/caelestia/utils/scheme.py new file mode 100644 index 0000000..0d6cfb5 --- /dev/null +++ b/src/caelestia/utils/scheme.py @@ -0,0 +1,224 @@ +import json +import random +from pathlib import Path + +from caelestia.utils.material import get_colours_for_image +from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path + + +class Scheme: + _name: str + _flavour: str + _mode: str + _variant: str + _colours: dict[str, str] + + def __init__(self, json: dict[str, any] | None) -> None: + if json is None: + self._name = "catppuccin" + self._flavour = "mocha" + self._mode = "dark" + self._variant = "tonalspot" + self._colours = read_colours_from_file(self.get_colours_path()) + else: + self._name = json["name"] + self._flavour = json["flavour"] + self._mode = json["mode"] + self._variant = json["variant"] + self._colours = json["colours"] + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, name: str) -> None: + if name == self._name: + return + + if name not in get_scheme_names(): + raise ValueError(f"Invalid scheme name: {name}") + + self._name = name + self._check_flavour() + self._check_mode() + self._update_colours() + self.save() + + @property + def flavour(self) -> str: + return self._flavour + + @flavour.setter + def flavour(self, flavour: str) -> None: + if flavour == self._flavour: + return + + if flavour not in get_scheme_flavours(): + raise ValueError(f'Invalid scheme flavour: "{flavour}". Valid flavours: {get_scheme_flavours()}') + + self._flavour = flavour + self._check_mode() + self.update_colours() + + @property + def mode(self) -> str: + return self._mode + + @mode.setter + def mode(self, mode: str) -> None: + if mode == self._mode: + return + + if mode not in get_scheme_modes(): + raise ValueError(f'Invalid scheme mode: "{mode}". Valid modes: {get_scheme_modes()}') + + self._mode = mode + self.update_colours() + + @property + def variant(self) -> str: + return self._variant + + @variant.setter + def variant(self, variant: str) -> None: + if variant == self._variant: + return + + self._variant = variant + self.update_colours() + + @property + def colours(self) -> dict[str, str]: + return self._colours + + def get_colours_path(self) -> Path: + return (scheme_data_dir / self.name / self.flavour / self.mode).with_suffix(".txt") + + def save(self) -> None: + scheme_path.parent.mkdir(parents=True, exist_ok=True) + atomic_dump( + scheme_path, + { + "name": self.name, + "flavour": self.flavour, + "mode": self.mode, + "variant": self.variant, + "colours": self.colours, + }, + ) + + def set_random(self) -> None: + self._name = random.choice(get_scheme_names()) + self._flavour = random.choice(get_scheme_flavours()) + self._mode = random.choice(get_scheme_modes()) + self.update_colours() + + def update_colours(self) -> None: + self._update_colours() + self.save() + + def _check_flavour(self) -> None: + global scheme_flavours + scheme_flavours = None + if self._flavour not in get_scheme_flavours(): + self._flavour = get_scheme_flavours()[0] + + def _check_mode(self) -> None: + global scheme_modes + scheme_modes = None + if self._mode not in get_scheme_modes(): + self._mode = get_scheme_modes()[0] + + def _update_colours(self) -> None: + if self.name == "dynamic": + self._colours = get_colours_for_image() + else: + self._colours = read_colours_from_file(self.get_colours_path()) + + def __str__(self) -> str: + return ( + f"Current scheme:\n" + f" Name: {self.name}\n" + f" Flavour: {self.flavour}\n" + f" Mode: {self.mode}\n" + f" Variant: {self.variant}\n" + f" Colours:\n" + f" {'\n '.join(f'{n}: \x1b[38;2;{int(c[0:2], 16)};{int(c[2:4], 16)};{int(c[4:6], 16)}m{c}\x1b[0m' for n, c in self.colours.items())}" + ) + + +scheme_variants = [ + "tonalspot", + "vibrant", + "expressive", + "fidelity", + "fruitsalad", + "monochrome", + "neutral", + "rainbow", + "content", +] + +scheme_names: list[str] = None +scheme_flavours: list[str] = None +scheme_modes: list[str] = None + +scheme: Scheme = None + + +def read_colours_from_file(path: Path) -> dict[str, str]: + return {k.strip(): v.strip() for k, v in (line.split(" ") for line in path.read_text().splitlines())} + + +def get_scheme_path() -> Path: + return get_scheme().get_colours_path() + + +def get_scheme() -> Scheme: + global scheme + + if scheme is None: + try: + scheme_json = json.loads(scheme_path.read_text()) + scheme = Scheme(scheme_json) + except (IOError, json.JSONDecodeError): + scheme = Scheme(None) + + return scheme + + +def get_scheme_names() -> list[str]: + global scheme_names + + if scheme_names is None: + scheme_names = [f.name for f in scheme_data_dir.iterdir() if f.is_dir()] + scheme_names.append("dynamic") + + return scheme_names + + +def get_scheme_flavours() -> list[str]: + global scheme_flavours + + if scheme_flavours is None: + name = get_scheme().name + if name == "dynamic": + scheme_flavours = ["default", "alt1", "alt2"] + else: + scheme_flavours = [f.name for f in (scheme_data_dir / name).iterdir() if f.is_dir()] + + return scheme_flavours + + +def get_scheme_modes() -> list[str]: + global scheme_modes + + if scheme_modes is None: + scheme = get_scheme() + if scheme.name == "dynamic": + scheme_modes = ["light", "dark"] + else: + scheme_modes = [f.stem for f in (scheme_data_dir / scheme.name / scheme.flavour).iterdir() if f.is_file()] + + return scheme_modes diff --git a/src/caelestia/utils/theme.py b/src/caelestia/utils/theme.py new file mode 100644 index 0000000..d205fb1 --- /dev/null +++ b/src/caelestia/utils/theme.py @@ -0,0 +1,122 @@ +import subprocess +import tempfile +from pathlib import Path + +from caelestia.utils.paths import config_dir, templates_dir + + +def gen_conf(colours: dict[str, str]) -> str: + conf = "" + for name, colour in colours.items(): + conf += f"${name} = {colour}\n" + return conf + + +def gen_scss(colours: dict[str, str]) -> str: + scss = "" + for name, colour in colours.items(): + scss += f"${name}: #{colour};\n" + return scss + + +def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) -> str: + template = template.read_text() + for name, colour in colours.items(): + template = template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour) + return template + + +def c2s(c: str, *i: list[int]) -> str: + """Hex to ANSI sequence (e.g. ffffff, 11 -> \x1b]11;rgb:ff/ff/ff\x1b\\)""" + return f"\x1b]{';'.join(map(str, i))};rgb:{c[0:2]}/{c[2:4]}/{c[4:6]}\x1b\\" + + +def gen_sequences(colours: dict[str, str]) -> str: + """ + 10: foreground + 11: background + 12: cursor + 17: selection + 4: + 0 - 7: normal colours + 8 - 15: bright colours + 16+: 256 colours + """ + return ( + c2s(colours["onSurface"], 10) + + c2s(colours["surface"], 11) + + c2s(colours["secondary"], 12) + + c2s(colours["secondary"], 17) + + c2s(colours["surfaceContainer"], 4, 0) + + c2s(colours["red"], 4, 1) + + c2s(colours["green"], 4, 2) + + c2s(colours["yellow"], 4, 3) + + c2s(colours["blue"], 4, 4) + + c2s(colours["pink"], 4, 5) + + c2s(colours["teal"], 4, 6) + + c2s(colours["onSurfaceVariant"], 4, 7) + + c2s(colours["surfaceContainer"], 4, 8) + + c2s(colours["red"], 4, 9) + + c2s(colours["green"], 4, 10) + + c2s(colours["yellow"], 4, 11) + + c2s(colours["blue"], 4, 12) + + c2s(colours["pink"], 4, 13) + + c2s(colours["teal"], 4, 14) + + c2s(colours["onSurfaceVariant"], 4, 15) + + c2s(colours["primary"], 4, 16) + + c2s(colours["secondary"], 4, 17) + + c2s(colours["tertiary"], 4, 18) + ) + + +def try_write(path: Path, content: str) -> None: + try: + path.write_text(content) + except FileNotFoundError: + pass + + +def apply_terms(sequences: str) -> None: + pts_path = Path("/dev/pts") + for pt in pts_path.iterdir(): + if pt.name.isdigit(): + with pt.open("a") as f: + f.write(sequences) + + +def apply_hypr(conf: str) -> None: + try_write(config_dir / "hypr/scheme/current.conf", conf) + + +def apply_discord(scss: str) -> None: + with tempfile.TemporaryDirectory("w") as tmp_dir: + (Path(tmp_dir) / "_colours.scss").write_text(scss) + conf = subprocess.check_output(["sass", "-I", tmp_dir, templates_dir / "discord.scss"], text=True) + + for client in "Equicord", "Vencord", "BetterDiscord", "equicord", "vesktop", "legcord": + try_write(config_dir / client / "themes/caelestia.theme.css", conf) + + +def apply_spicetify(colours: dict[str, str], mode: str) -> None: + template = gen_replace(colours, templates_dir / f"spicetify-{mode}.ini") + try_write(config_dir / "spicetify/Themes/caelestia/color.ini", template) + + +def apply_fuzzel(colours: dict[str, str]) -> None: + template = gen_replace(colours, templates_dir / "fuzzel.ini") + try_write(config_dir / "fuzzel/fuzzel.ini", template) + + +def apply_btop(colours: dict[str, str]) -> None: + template = gen_replace(colours, templates_dir / "btop.theme", hash=True) + try_write(config_dir / "btop/themes/caelestia.theme", template) + subprocess.run(["killall", "-USR2", "btop"]) + + +def apply_colours(colours: dict[str, str], mode: str) -> None: + apply_terms(gen_sequences(colours)) + apply_hypr(gen_conf(colours)) # FIXME: LAGGY + apply_discord(gen_scss(colours)) + apply_spicetify(colours, mode) + apply_fuzzel(colours) + apply_btop(colours) diff --git a/src/caelestia/utils/wallpaper.py b/src/caelestia/utils/wallpaper.py new file mode 100644 index 0000000..0a666be --- /dev/null +++ b/src/caelestia/utils/wallpaper.py @@ -0,0 +1,139 @@ +import random +from argparse import Namespace +from pathlib import Path + +from materialyoucolor.hct import Hct +from materialyoucolor.utils.color_utils import argb_from_rgb +from PIL import Image + +from caelestia.utils.hypr import message +from caelestia.utils.material import get_colours_for_image +from caelestia.utils.paths import ( + compute_hash, + wallpaper_link_path, + wallpaper_path_path, + wallpaper_thumbnail_path, + wallpapers_cache_dir, +) +from caelestia.utils.scheme import Scheme, get_scheme +from caelestia.utils.theme import apply_colours + + +def is_valid_image(path: Path | str) -> bool: + path = Path(path) + return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"] + + +def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bool: + with Image.open(wall) as img: + width, height = img.size + return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold + + +def get_wallpaper() -> str: + try: + return wallpaper_path_path.read_text() + except IOError: + return None + + +def get_wallpapers(args: Namespace) -> list[Path]: + dir = Path(args.random) + if not dir.is_dir(): + return [] + + walls = [f for f in dir.rglob("*") if is_valid_image(f)] + + if args.no_filter: + return walls + + monitors = message("monitors") + filter_size = monitors[0]["width"], monitors[0]["height"] + for monitor in monitors[1:]: + if filter_size[0] > monitor["width"]: + filter_size[0] = monitor["width"] + if filter_size[1] > monitor["height"]: + filter_size[1] = monitor["height"] + + return [f for f in walls if check_wall(f, filter_size, args.threshold)] + + +def get_thumb(wall: Path, cache: Path) -> Path: + thumb = cache / "thumbnail.jpg" + + if not thumb.exists(): + with Image.open(wall) as img: + img = img.convert("RGB") + img.thumbnail((128, 128), Image.NEAREST) + thumb.parent.mkdir(parents=True, exist_ok=True) + img.save(thumb, "JPEG") + + return thumb + + +def get_smart_mode(wall: Path, cache: Path) -> str: + mode_cache = cache / "mode.txt" + + try: + return mode_cache.read_text() + except IOError: + with Image.open(get_thumb(wall, cache)) as img: + img.thumbnail((1, 1), Image.LANCZOS) + mode = "light" if Hct.from_int(argb_from_rgb(*img.getpixel((0, 0)))).tone > 60 else "dark" + + mode_cache.parent.mkdir(parents=True, exist_ok=True) + mode_cache.write_text(mode) + + return mode + + +def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None: + scheme = get_scheme() + cache = wallpapers_cache_dir / compute_hash(wall) + + if not no_smart: + scheme = Scheme( + { + "name": scheme.name, + "flavour": scheme.flavour, + "mode": get_smart_mode(wall, cache), + "variant": scheme.variant, + "colours": scheme.colours, + } + ) + + return get_colours_for_image(get_thumb(wall, cache), scheme) + + +def set_wallpaper(wall: Path | str, no_smart: bool) -> None: + if not is_valid_image(wall): + raise ValueError(f'"{wall}" is not a valid image') + + # Update files + wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True) + wallpaper_path_path.write_text(str(wall)) + wallpaper_link_path.parent.mkdir(parents=True, exist_ok=True) + wallpaper_link_path.unlink(missing_ok=True) + wallpaper_link_path.symlink_to(wall) + + cache = wallpapers_cache_dir / compute_hash(wall) + + # Generate thumbnail or get from cache + thumb = get_thumb(wall, cache) + wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True) + wallpaper_thumbnail_path.unlink(missing_ok=True) + wallpaper_thumbnail_path.symlink_to(thumb) + + scheme = get_scheme() + + # Change mode based on wallpaper colour + if not no_smart: + scheme.mode = get_smart_mode(wall, cache) + + # Update colours + scheme.update_colours() + apply_colours(scheme.colours, scheme.mode) + + +def set_random(args: Namespace) -> None: + set_wallpaper(random.choice(get_wallpapers(args)), args.no_smart) diff --git a/toggles/specialws.fish b/toggles/specialws.fish deleted file mode 100755 index 01fcfd7..0000000 --- a/toggles/specialws.fish +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env fish - -if ! hyprctl workspaces -j | jq -e 'first(.[] | select(.name == "special:special"))' - set activews (hyprctl activewindow -j | jq -r '.workspace.name') - string match -r -- '^special:' $activews && set togglews (string sub -s 9 $activews) || set togglews special -else - set togglews special -end - -hyprctl dispatch togglespecialworkspace $togglews diff --git a/toggles/util.fish b/toggles/util.fish deleted file mode 100644 index 3cffc15..0000000 --- a/toggles/util.fish +++ /dev/null @@ -1,42 +0,0 @@ -. (dirname (status filename))/../util.fish - -function move-client -a selector workspace - if hyprctl -j clients | jq -e 'first(.[] | select('$selector')).workspace.name != "special:'$workspace'"' > /dev/null - # Window not in correct workspace - set -l window_addr (hyprctl -j clients | jq -r 'first(.[] | select('$selector')).address') - hyprctl dispatch movetoworkspacesilent "special:$workspace,address:$window_addr" - end -end - -function spawn-client -a selector spawn - # Spawn if doesn't exist - hyprctl -j clients | jq -e "first(.[] | select($selector))" > /dev/null - set -l stat $status - if test $stat != 0 - eval "app2unit -- $spawn & disown" - end - test $stat != 0 # Exit 1 if already exists -end - -function jq-var -a op json - jq -rn --argjson json "$json" "\$json | $op" -end - -function toggle-workspace -a workspace - set -l apps (get-config "toggles.$workspace.apps") - - for i in (seq 0 (math (jq-var 'length' "$apps") - 1)) - set -l app (jq-var ".[$i]" "$apps") - set -l action (jq-var '.action' "$app") - set -l selector (jq-var '.selector' "$app") - set -l extra_cond (jq-var '.extraCond' "$app") - - test $extra_cond = null && set -l extra_cond true - if eval $extra_cond - string match -qe -- 'spawn' $action && spawn-client $selector (jq-var '.spawn' "$app") - string match -qe -- 'move' $action && move-client $selector $workspace - end - end - - hyprctl dispatch togglespecialworkspace $workspace -end diff --git a/util.fish b/util.fish deleted file mode 100644 index 5718628..0000000 --- a/util.fish +++ /dev/null @@ -1,51 +0,0 @@ -function _out -a colour level text - set_color $colour - # Pass arguments other than text to echo - echo $argv[4..] -- ":: [$level] $text" - set_color normal -end - -function log -a text - _out cyan LOG $text $argv[2..] -end - -function warn -a text - _out yellow WARN $text $argv[2..] -end - -function error -a text - _out red ERROR $text $argv[2..] - return 1 -end - -function input -a text - _out blue INPUT $text $argv[2..] -end - -function get-config -a key - test -f $C_CONFIG_FILE && set -l value (jq -r ".$key" $C_CONFIG_FILE) - test -n "$value" -a "$value" != null && echo $value || jq -r ".$key" (dirname (status filename))/data/config.json -end - -function set-config -a key value - if test -f $C_CONFIG_FILE - set -l tmp (mktemp) - cp $C_CONFIG_FILE $tmp - jq -e ".$key = $value" $tmp > $C_CONFIG_FILE || cp $tmp $C_CONFIG_FILE - rm $tmp - else - jq -en ".$key = $value" > $C_CONFIG_FILE || rm $C_CONFIG_FILE - end -end - -set -q XDG_DATA_HOME && set C_DATA $XDG_DATA_HOME/caelestia || set C_DATA $HOME/.local/share/caelestia -set -q XDG_STATE_HOME && set C_STATE $XDG_STATE_HOME/caelestia || set C_STATE $HOME/.local/state/caelestia -set -q XDG_CACHE_HOME && set C_CACHE $XDG_CACHE_HOME/caelestia || set C_CACHE $HOME/.cache/caelestia -set -q XDG_CONFIG_HOME && set CONFIG $XDG_CONFIG_HOME || set CONFIG $HOME/.config -set C_CONFIG $CONFIG/caelestia -set C_CONFIG_FILE $C_CONFIG/scripts.json - -mkdir -p $C_DATA -mkdir -p $C_STATE -mkdir -p $C_CACHE -mkdir -p $C_CONFIG diff --git a/wallpaper.fish b/wallpaper.fish deleted file mode 100755 index e8cfdd8..0000000 --- a/wallpaper.fish +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env fish - -function get-valid-wallpapers - identify -ping -format '%i\n' $wallpapers_dir/** 2> /dev/null -end - -set script_name (basename (status filename)) -set wallpapers_dir (xdg-user-dir PICTURES)/Wallpapers -set threshold 80 - -# Max 0 non-option args | h, f and d are exclusive | F and t are also exclusive -argparse -n 'caelestia-wallpaper' -X 0 -x 'h,f,d' -x 'F,t' \ - 'h/help' \ - 'f/file=' \ - 'd/directory=' \ - 'F/no-filter' \ - 't/threshold=!_validate_int --min 0' \ - 'T/theme=!test $_flag_value = light -o $_flag_value = dark' \ - -- $argv -or exit - -. (dirname (status filename))/util.fish - -if set -q _flag_h - echo 'Usage:' - echo ' caelestia wallpaper' - echo ' caelestia wallpaper [ -h | --help ]' - echo ' caelestia wallpaper [ -f | --file ] [ -T | --theme ]' - echo ' caelestia wallpaper [ -d | --directory ] [ -F | --no-filter ] [ -T | --theme ]' - echo ' caelestia wallpaper [ -d | --directory ] [ -t | --threshold ] [ -T | --theme ]' - echo - echo 'Options:' - echo ' -h, --help Print this help message and exit' - echo ' -f, --file <file> The file to change wallpaper to' - echo ' -d, --directory <directory> The folder to select a random wallpaper from (default '$wallpapers_dir')' - echo ' -F, --no-filter Do not filter by size' - echo ' -t, --threshold <threshold> The minimum percentage of the size the image must be greater than to be selected (default '$threshold')' - echo ' -T, --theme <"light" | "dark"> Set light/dark theme for dynamic scheme' -else - set state_dir $C_STATE/wallpaper - - # The path to the last chosen wallpaper - set last_wallpaper_path "$state_dir/last.txt" - - # Use wallpaper given as argument else choose random - if set -q _flag_f - set chosen_wallpaper (realpath $_flag_f) - - if ! identify -ping $chosen_wallpaper &> /dev/null - error "$chosen_wallpaper is not a valid image" - exit 1 - end - else - # The path to the directory containing the selection of wallpapers - set -q _flag_d && set wallpapers_dir (realpath $_flag_d) - - if ! test -d $wallpapers_dir - error "$wallpapers_dir does not exist" - exit 1 - end - - # Get all files in $wallpapers_dir and exclude the last wallpaper (if it exists) - if test -f "$last_wallpaper_path" - set last_wallpaper (cat $last_wallpaper_path) - test -n "$last_wallpaper" && set unfiltered_wallpapers (get-valid-wallpapers | grep -v $last_wallpaper) - end - set -q unfiltered_wallpapers || set unfiltered_wallpapers (get-valid-wallpapers) - - # Filter by resolution if no filter option is not given - if set -q _flag_F - set wallpapers $unfiltered_wallpapers - else - set -l screen_size (hyprctl monitors -j | jq -r 'max_by(.width * .height) | "\(.width)\n\(.height)"') - set -l wall_sizes (identify -ping -format '%w %h\n' $unfiltered_wallpapers) - - # Apply threshold - set -q _flag_t && set threshold $_flag_t - set screen_size[1] (math $screen_size[1] x $threshold / 100) - set screen_size[2] (math $screen_size[2] x $threshold / 100) - - # Add wallpapers that are larger than the screen size * threshold to list to choose from ($wallpapers) - for i in (seq 1 (count $wall_sizes)) - set -l wall_size (string split ' ' $wall_sizes[$i]) - if test $wall_size[1] -ge $screen_size[1] -a $wall_size[2] -ge $screen_size[2] - set -a wallpapers $unfiltered_wallpapers[$i] - end - end - end - - # Check if the $wallpapers list is unset or empty - if ! set -q wallpapers || test -z "$wallpapers" - error "No valid images found in $wallpapers_dir" - exit 1 - end - - # Choose a random wallpaper from the $wallpapers list - set chosen_wallpaper (random choice $wallpapers) - end - - # Thumbnail wallpaper for colour gen - mkdir -p $C_CACHE/thumbnails - set -l thumb_path $C_CACHE/thumbnails/(sha1sum $chosen_wallpaper | cut -d ' ' -f 1).jpg - if ! test -f $thumb_path - magick -define jpeg:size=256x256 $chosen_wallpaper -thumbnail 128x128 $thumb_path - end - cp $thumb_path $state_dir/thumbnail.jpg - - # Light/dark mode detection if not specified - if ! set -q _flag_T - set -l lightness (magick $state_dir/thumbnail.jpg -format '%[fx:int(mean*100)]' info:) - test $lightness -ge 60 && set _flag_T light || set _flag_T dark - end - - # Generate colour scheme for wallpaper - set -l src (dirname (status filename)) - MODE=$_flag_T $src/scheme/gen-scheme.fish & - - # Store the wallpaper chosen - mkdir -p $state_dir - echo $chosen_wallpaper > $last_wallpaper_path - ln -sf $chosen_wallpaper "$state_dir/current" -end diff --git a/workspace-action.sh b/workspace-action.sh deleted file mode 100755 index 76d0950..0000000 --- a/workspace-action.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -active_ws=$(hyprctl activeworkspace -j | jq -r '.id') - -if [[ "$1" == *"group" ]]; then - # Move to group - hyprctl dispatch "${1::-5}" $((($2 - 1) * 10 + ${active_ws:0-1})) -else - # Move to ws in group - hyprctl dispatch "$1" $((((active_ws - 1) / 10) * 10 + $2)) -fi |