Merge pull request #6 from caelestia-dots/python-rework

feat: rewrite in python
This commit is contained in:
2 * r + 2 * t
2025-06-14 22:50:55 +10:00
committed by GitHub
86 changed files with 2314 additions and 1652 deletions
+2 -1
View File
@@ -1 +1,2 @@
/data/schemes/dynamic/
__pycache__/
/dist/
+9 -26
View File
@@ -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
### Manual installation
TODO
## Usage
```
> caelestia help
Usage: caelestia COMMAND [ ...args ]
COMMAND := help | install | shell | toggle | workspace-action | scheme | screenshot | record | clipboard | clipboard-delete | emoji-picker | wallpaper | pip
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
-4
View File
@@ -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
-4
View File
@@ -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
+30 -60
View File
@@ -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'
# Clipboard
complete -c caelestia -n "$seen clipboard" -s 'd' -l 'delete' -d 'Delete from cliboard history'
# 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
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
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'
-51
View File
@@ -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"
}
]
}
}
}
-4
View File
@@ -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
-16
View File
@@ -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.'
-26
View File
@@ -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.'
-36
View File
@@ -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'
-24
View File
@@ -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.'
-14
View File
@@ -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.'
-15
View File
@@ -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.'
-23
View File
@@ -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.'
-32
View File
@@ -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.'
-20
View File
@@ -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.'
-34
View File
@@ -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.'
-21
View File
@@ -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.'
-42
View File
@@ -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.'
-18
View File
@@ -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.'
-24
View File
@@ -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
-148
View File
@@ -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
-33
View File
@@ -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.'
-115
View File
@@ -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
-61
View File
@@ -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
+14
View File
@@ -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"
-99
View File
@@ -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
Executable
+11
View File
@@ -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 "$@"
-256
View File
@@ -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))
-36
View File
@@ -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
-33
View File
@@ -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
-81
View File
@@ -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
-41
View File
@@ -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
+13
View File
@@ -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()
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+83
View File
@@ -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 }}
+174
View File
@@ -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%)};
}
+41
View File
@@ -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
@@ -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
@@ -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
+112
View File
@@ -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()
+25
View File
@@ -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)
+18
View File
@@ -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)
+44
View File
@@ -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)
+122
View File
@@ -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()
+30
View File
@@ -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)
+78
View File
@@ -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}"])
+41
View File
@@ -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="")
+75
View File
@@ -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)
+21
View File
@@ -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")
+18
View File
@@ -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)
+29
View File
@@ -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"
+52
View File
@@ -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
+194
View File
@@ -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()}
+16 -18
View File
@@ -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]))
+53
View File
@@ -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)
+224
View File
@@ -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
+122
View File
@@ -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)
+139
View File
@@ -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)
-10
View File
@@ -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
-42
View File
@@ -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
-51
View File
@@ -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
-122
View File
@@ -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
-11
View File
@@ -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