40 Commits

Author SHA1 Message Date
2 * r + 2 * t a00e71d6b7 docs: add iconTheme options to example conf 2026-05-02 22:51:35 +10:00
2 * r + 2 * t 1ec969d9ec fix: use auto bars for cava 2026-05-02 22:48:41 +10:00
2 * r + 2 * t 5273ed514f feat: add theme postHook 2026-05-02 22:24:26 +10:00
github-actions f3b13affc3 [CI] chore: update flake 2026-05-02 03:39:49 +00:00
Haikal 5c9ce66c03 feat: expose more environment variables in post-hook (#107)
* feat: expose more environment variables in post-hook

* fix: formatted
2026-04-29 23:56:07 +10:00
github-actions c18f749f24 [CI] chore: update flake 2026-04-29 03:43:15 +00:00
2 * r + 2 * t 96fcdf5bce fix: use hypr socket instead of hyprctl 2026-04-28 21:20:40 +10:00
github-actions eddee4deca [CI] chore: update flake 2026-04-25 03:06:00 +00:00
Foxlike Creature 68bc03bc17 feat: allow overriding icon theme via cli.json (#106)
* theme: allow overriding Qt icon theme via cli.json

Papirus colors XDG special folders (Downloads, Pictures, Music, etc.)
differently from regular ones - they end up a different color while
everything else stays neutral. With themes like breeze-dark, all folder
icons share the same style, so everything looks consistent.

Add optional `iconTheme` field to the `theme` section of cli.json.
When set, it replaces the Papirus icon theme in the generated qtengine
config with the specified theme.

Example usage in cli.json:
  "theme": { "iconTheme": "breeze-dark" }

* theme: allow overriding Qt and GTK icon theme via cli.json

Some folders in Dolphin end up with Papirus-style icons while others
use the default theme icons, resulting in two different icon styles
mixed together in the same view. Dolphin's default folder icons take
their color directly from the active color scheme, so they always match
the theme exactly - Papirus has a fixed, limited palette and does not
always match.

Add optional iconThemeDark and iconThemeLight fields to the theme
section of cli.json. When set, they override the Papirus icon theme in
both the generated qtengine config and the GTK dconf setting. A generic
iconTheme field is also supported as a fallback for both modes.

Example usage in cli.json:
  "theme": { "iconThemeDark": "breeze-dark", "iconThemeLight": "breeze" }

---------

Co-authored-by: Foxlike Creature <safonovkirill113@gmail.com>
2026-04-24 14:55:47 +10:00
github-actions 023a30b83c [CI] chore: update flake 2026-04-21 03:27:51 +00:00
github-actions a192efae9c [CI] chore: update flake 2026-04-20 03:35:55 +00:00
2 * r + 2 * t 463f36544a docs: add missing theme.enable* opts to example conf 2026-04-19 16:01:09 +10:00
Yuka 5f1d008cce theme: add live theming for chromium-based browsers (#103)
* theme: add live theming for chromium-based browsers

Writes the surface colour as an RGB triplet to chromium.theme and
applies it as a managed browser policy for chromium, brave, and
google-chrome-stable using the --refresh-platform-policy flag
(introduced in Chrome 142+).

Also fixes write_file to set 0644 permissions so browser processes
can read the policy files.

* feat: use sudo tee not chmod

* chore: update readme

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2026-04-15 16:20:15 +10:00
2 * r + 2 * t 66bce26841 fix: remove troublesome resizer rules 2026-04-15 02:18:30 +10:00
Yuka e1531f3c9e theme: add zed editor theme support (#102)
- Add Zed theme template (zed.json) with full Material You syntax
  highlighting, UI colors, and terminal ANSI palette
- Apply theme via file overwrite instead of restarting Zed, so
  Zed's file watcher picks up changes automatically
- Resolve symlinks in apply_zed() only before writing, since
  Zed's file watcher does not detect changes through symlinks
  (other themed apps that rely on symlinks are unaffected)
2026-04-12 17:28:44 +10:00
github-actions 7f59ca9656 [CI] chore: update flake 2026-04-11 02:50:24 +00:00
2 * r + 2 * t 4cf9e8603f feat: add caelestia scheme
Closes #100
2026-04-10 21:24:51 +10:00
github-actions b4b26ab5d5 [CI] chore: update flake 2026-04-09 02:53:38 +00:00
github-actions b4758901f9 [CI] chore: update flake 2026-04-07 02:57:24 +00:00
github-actions 60284ca41e [CI] chore: update flake 2026-04-04 02:45:02 +00:00
github-actions a019c42244 [CI] chore: update flake 2026-04-03 02:54:20 +00:00
github-actions 950c40bbd4 [CI] chore: update flake 2026-04-02 02:51:37 +00:00
github-actions d054129db8 [CI] chore: update flake 2026-04-01 03:26:37 +00:00
github-actions e497ca87eb [CI] chore: update flake 2026-03-31 02:55:25 +00:00
github-actions e81b1b87f0 [CI] chore: update flake 2026-03-30 03:22:12 +00:00
github-actions 1b823554ad [CI] chore: update flake 2026-03-29 03:19:20 +00:00
github-actions 4538e9cb50 [CI] chore: update flake 2026-03-28 02:46:03 +00:00
github-actions e5c161d43a [CI] chore: update flake 2026-03-27 02:56:29 +00:00
github-actions 11bdbc9e80 [CI] chore: update flake 2026-03-26 02:52:54 +00:00
github-actions 5bb3276b14 [CI] chore: update flake 2026-03-24 02:49:18 +00:00
github-actions 3930ca09b9 [CI] chore: update flake 2026-03-23 02:50:37 +00:00
github-actions 305f02d8aa [CI] chore: update flake 2026-03-22 02:50:36 +00:00
github-actions 8c7eea556d [CI] chore: update flake 2026-03-21 02:34:07 +00:00
AteebXYZ 6bb09e7703 readme: document user templates (#99)
* document user_templates in README

* move user_template documentation to Usage

* format

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2026-03-20 20:56:12 +11:00
github-actions b023936e1d [CI] chore: update flake 2026-03-20 02:40:16 +00:00
github-actions 3cdc2ce464 [CI] chore: update flake 2026-03-19 02:48:41 +00:00
github-actions 534d11d66a [CI] chore: update flake 2026-03-18 02:48:20 +00:00
github-actions 0d8be30138 [CI] chore: update flake 2026-03-17 02:42:09 +00:00
Samiyel Frazier 8c6d933267 nix: fix qtct sub (#98) 2026-03-16 14:14:46 +11:00
Kalagmitan b00c601d0a refactor: enforce stricter type hints (#91)
LSP was screaming at me so I decided to just address it to get it off my
screen.

+ Fixed the type hints
:= Modified and added type hints for certain functions and variables in
most of the files in the utils/ folder (and some in the subcommands/
folder) for clarity and so pyright's type checker wouldn't cry.
:+ To resolve certain type issues, I had to add a bit more tiny
additional code such as, additional checks if a variable is None, a tiny
class in utils/material/generator.py to resolve the constructor usage
mismatch between what the DynamicScheme accepts and what the code
actually passes, and etc.
- Renamed certain functions and variables for clarity and also for some
to not collide with pre-existing definitions from well-known library
imports.
+ PIL has reorganized their code a bit, so the code is made to reflect
their new definitions.
= Reorganized the single import statement for "colourfulness" in
utils/wallpaper.py to be close to the top.
(I think that's it)

Side Effects?:
Everything should work the same as no logic change was done whatsover
(unless we consider the added if statements for type checking as a logic
change). I've tested it, everything seems to be in urdir.
2026-03-15 22:56:05 +11:00
21 changed files with 1035 additions and 192 deletions
+81 -39
View File
@@ -4,43 +4,17 @@ The main control script for the Caelestia dotfiles.
<details><summary id="dependencies">External dependencies</summary>
- [`libnotfy`](https://gitlab.gnome.org/GNOME/libnotify) - sending notifications
- [`swappy`](https://github.com/jtheoof/swappy) - screenshot editor
- [`grim`](https://gitlab.freedesktop.org/emersion/grim) - taking screenshots
- [`dart-sass`](https://github.com/sass/dart-sass) - discord theming
- [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps
- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard
- [`slurp`](https://github.com/emersion/slurp) - selecting an area
- [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording
- `glib2` - closing notifications
- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history
- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
</details>
<details><summary id="optional-dependencies">Optional dependencies</summary>
- [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders) - automatic folder icon color syncing with theme
> [!NOTE]
> For automatic Papirus folder icon color syncing, `papirus-folders` needs to be able to run with `sudo` without a password prompt.
>
> **Recommended** - Create a sudoers file:
> ```fish
> # Fish shell
> echo "$USER ALL=(ALL) NOPASSWD: "(which papirus-folders) | sudo tee /etc/sudoers.d/papirus-folders
> sudo chmod 440 /etc/sudoers.d/papirus-folders
> ```
> ```sh
> # Bash/other shells
> echo "$USER ALL=(ALL) NOPASSWD: $(which papirus-folders)" | sudo tee /etc/sudoers.d/papirus-folders
> sudo chmod 440 /etc/sudoers.d/papirus-folders
> ```
>
> **Alternatively** - Edit the main sudoers file by running `sudo visudo` and adding at the end:
> ```
> your_username ALL=(ALL) NOPASSWD: /usr/bin/papirus-folders
> ```
- [`libnotfy`](https://gitlab.gnome.org/GNOME/libnotify) - sending notifications
- [`swappy`](https://github.com/jtheoof/swappy) - screenshot editor
- [`grim`](https://gitlab.freedesktop.org/emersion/grim) - taking screenshots
- [`dart-sass`](https://github.com/sass/dart-sass) - discord theming
- [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps
- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard
- [`slurp`](https://github.com/emersion/slurp) - selecting an area
- [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording
- `glib2` - closing notifications
- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history
- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
</details>
@@ -119,6 +93,45 @@ sudo python -m installer dist/*.whl
sudo cp completions/caelestia.fish /usr/share/fish/vendor_completions.d/caelestia.fish
```
### Additional steps
#### Auto folder colour theming
For automatic Papirus folder icon colour syncing, you must have [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders)
installed, and `papirus-folders` must to be able to run with `sudo` without a password prompt.
You can allow this by creating a sudoers file:
```sh
echo "$USER ALL=(ALL) NOPASSWD: $(which papirus-folders)" | sudo tee /etc/sudoers.d/papirus-folders
sudo chmod 440 /etc/sudoers.d/papirus-folders
```
#### Chromium-based browser theming
For live Chromium-based browser theming, the CLI must be allowed to create certain directories in `/etc`
and write to them via `sudo` without a password prompt.
You can allow this by creating a sudoers file:
```fish
# Fish shell
for dir in /etc/chromium/policies/managed /etc/brave/policies/managed /etc/opt/chrome/policies/managed
echo "$USER ALL=(ALL) NOPASSWD: $(which mkdir) -p $dir" | sudo tee -a /etc/sudoers.d/caelestia-chromium
echo "$USER ALL=(ALL) NOPASSWD: $(which tee) $dir/caelestia.json" | sudo tee -a /etc/sudoers.d/caelestia-chromium
end
sudo chmod 440 /etc/sudoers.d/caelestia-chromium
```
```sh
# Bash/other shells
for dir in /etc/chromium/policies/managed /etc/brave/policies/managed /etc/opt/chrome/policies/managed; do
echo "$USER ALL=(ALL) NOPASSWD: $(which mkdir) -p $dir" | sudo tee -a /etc/sudoers.d/caelestia-chromium
echo "$USER ALL=(ALL) NOPASSWD: $(which tee) $dir/caelestia.json" | sudo tee -a /etc/sudoers.d/caelestia-chromium
done
sudo chmod 440 /etc/sudoers.d/caelestia-chromium
```
## Usage
All subcommands/options can be explored via the help flag.
@@ -148,6 +161,24 @@ subcommands:
resizer window resizer daemon
```
### User templates
Custom user templates can be defined in `~/.config/caelestia/templates/`.
#### Template syntax
`{{ <color>.<format> }}`
- `<color>` is a theme color role derived from the Material You color system (e.g. `primary`, `secondary`, `background`)
- `<format>` is the output format: `hex` or `rgb`
#### Examples
- `{{ primary.hex }}` outputs `3f4ba2`
- `{{ primary.rgb }}` outputs `rgb(193, 132, 207)`
Output files are written to `~/.local/state/caelestia/theme/`. You can symlink them to your desired locations.
## Configuring
All configuration options are in `~/.config/caelestia/cli.json`.
@@ -160,17 +191,28 @@ All configuration options are in `~/.config/caelestia/cli.json`.
"extraArgs": []
},
"wallpaper": {
"postHook": "echo $WALLPAPER_PATH"
"postHook": "echo $WALLPAPER_PATH $SCHEME_NAME $SCHEME_FLAVOUR $SCHEME_MODE $SCHEME_VARIANT $SCHEME_COLOURS"
},
"theme": {
"enableTerm": true,
"enableHypr": true,
"enableDiscord": true,
"enableSpicetify": true,
"enablePandora": true,
"enableFuzzel": true,
"enableBtop": true,
"enableNvtop": true,
"enableHtop": true,
"enableGtk": true,
"enableQt": true
"enableQt": true,
"enableWarp": true,
"enableChromium": true,
"enableZed": true,
"enableCava": true,
"iconTheme": "Papirus-Dark",
"iconThemeLight": "Papirus-Light",
"iconThemeDark": "Papirus-Dark",
"postHook": "echo $SCHEME_NAME $SCHEME_FLAVOUR $SCHEME_MODE $SCHEME_VARIANT $SCHEME_COLOURS"
},
"toggles": {
"communication": {
+1 -1
View File
@@ -72,7 +72,7 @@ python3.pkgs.buildPythonApplication {
--replace-fail 'app2unit' ${app2unit}/bin/app2unit
# Use config style instead of darkly
substituteInPlace src/caelestia/data/templates/qtct.conf \
substituteInPlace src/caelestia/data/templates/qtengine.json \
--replace-fail 'Darkly' '${qtctStyle}'
'';
Generated
+10 -10
View File
@@ -9,11 +9,11 @@
"quickshell": "quickshell"
},
"locked": {
"lastModified": 1772962569,
"narHash": "sha256-ctRw4pVgx0IYKfA2hy90Ku37pnVX2T4q57UWp+l69fs=",
"lastModified": 1777688289,
"narHash": "sha256-2EaEVkT1oUpjLLp7uEY/hDYDOa2k5R1YgcJpHei+lUM=",
"owner": "caelestia-dots",
"repo": "shell",
"rev": "e183599ce9e2c8d30a14631d53eb9947220c0812",
"rev": "4e9e1f4b723f7e3a87cb280d67a25ee92c87fbff",
"type": "github"
},
"original": {
@@ -24,11 +24,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1772773019,
"narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=",
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "aca4d95fce4914b3892661bcb80b8087293536c6",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
},
"original": {
@@ -46,11 +46,11 @@
]
},
"locked": {
"lastModified": 1772925576,
"narHash": "sha256-mMoiXABDtkSJxCYDrkhJ/TrrJf5M46oUfIlJvv2gkZ0=",
"lastModified": 1777341401,
"narHash": "sha256-QEAVYeXxvTamsYJVBq8+qSJV9ml2MxqRaZvkobfuPWA=",
"ref": "refs/heads/master",
"rev": "15a84097653593dd15fad59a56befc2b7bdc270d",
"revCount": 750,
"rev": "0baa81aa03559ca315668e5a306364cddf1a6f49",
"revCount": 812,
"type": "git",
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
},
@@ -0,0 +1,120 @@
background 0a0f0f
onBackground dce8e6
surface 0a0f0f
surfaceDim 0a0f0f
surfaceBright 242e2d
surfaceContainerLowest 000000
surfaceContainerLow 0e1514
surfaceContainer 131b1a
surfaceContainerHigh 192120
surfaceContainerHighest 1d2827
onSurface dce8e6
surfaceVariant 1d2827
onSurfaceVariant a2adac
outline 6d7876
outlineVariant 3f4a49
inverseSurface f6faf9
inverseOnSurface 515655
shadow 000000
scrim 000000
surfaceTint 9bd0cc
primary 9bd0cc
primaryDim 8ec2bf
onPrimary 0d4845
primaryContainer 255b58
onPrimaryContainer b8ede9
inversePrimary 336764
primaryFixed b7ede9
primaryFixedDim a9deda
onPrimaryFixed 0c4744
onPrimaryFixedVariant 306461
secondary b0ccc9
secondaryDim a3bebc
onSecondary 2c4543
secondaryContainer 27403e
onSecondaryContainer a9c5c2
secondaryFixed cce8e5
secondaryFixedDim bedad7
onSecondaryFixed 2b4442
onSecondaryFixedVariant 47605e
tertiary d5efff
tertiaryDim b6e3fe
onTertiary 2e5c72
tertiaryContainer b6e3fe
onTertiaryContainer 255369
tertiaryFixed b6e3fe
tertiaryFixedDim a8d5ef
onTertiaryFixed 0b4156
onTertiaryFixedVariant 2f5d73
error fa746f
errorDim c54d4a
onError 490006
errorContainer 871f21
onErrorContainer ff9993
primaryPaletteKeyColor 4c807d
secondaryPaletteKeyColor 627c7a
tertiaryPaletteKeyColor 517d94
neutralPaletteKeyColor 737877
neutralVariantPaletteKeyColor 6e7978
errorPaletteKeyColor c84f4c
primary_paletteKeyColor 4c807d
secondary_paletteKeyColor 627c7a
tertiary_paletteKeyColor 517d94
neutral_paletteKeyColor 737877
neutral_variant_paletteKeyColor 6e7978
term0 343434
term1 769e00
term2 56e2c0
term3 81fcce
term4 76b6b3
term5 7aaee9
term6 83d8c9
term7 cddcd3
term8 9aa59e
term9 85b900
term10 41f7d0
term11 cdffe9
term12 a3c8c3
term13 a2c0f7
term14 8bedd9
term15 ffffff
rosewater f1f3e5
flamingo e3e4c5
pink bae2ff
mauve 60cfe8
red 8ab5ff
maroon abbef0
peach a9daac
yellow d3fae8
green 8df1df
teal 9feee7
sky 93eae9
sapphire 70d7db
blue 57cdda
lavender 86d9e7
klink 00969e
klinkSelection 00969e
kvisited 008ca9
kvisitedSelection 008ca9
knegative 838f00
knegativeSelection 838f00
kneutral 34c359
kneutralSelection 34c359
kpositive 00beab
kpositiveSelection 00beab
text dce8e6
subtext1 a2adac
subtext0 6d7876
overlay2 5f6967
overlay1 505958
overlay0 434b4a
surface2 353d3c
surface1 282e2e
surface0 191f1e
base 0a0f0f
mantle 0a0f0f
crust 090e0e
success B5CCBA
onSuccess 213528
successContainer 374B3E
onSuccessContainer D1E9D6
@@ -0,0 +1,120 @@
background f6faf9
onBackground 2a3433
surface f6faf9
surfaceDim d1dcdb
surfaceBright f6faf9
surfaceContainerLowest ffffff
surfaceContainerLow eef5f3
surfaceContainer e7f0ee
surfaceContainerHigh e1eae8
surfaceContainerHighest d9e5e3
onSurface 2a3433
surfaceVariant d9e5e3
onSurfaceVariant 566160
outline 727d7c
outlineVariant a9b4b3
inverseSurface 0a0f0f
inverseOnSurface 999e9d
shadow 000000
scrim 000000
surfaceTint 1c6a66
primary 1c6a66
primaryDim 045d5a
onPrimary e1fffc
primaryContainer a8f0eb
onPrimaryContainer 015c59
inversePrimary b0f8f3
primaryFixed a8f0eb
primaryFixedDim 9ae1dc
onPrimaryFixed 004845
onPrimaryFixedVariant 166663
secondary 4a6462
secondaryDim 3e5856
onSecondary e2fffc
secondaryContainer cce8e5
onSecondaryContainer 3d5654
secondaryFixed cce8e5
secondaryFixedDim bedad7
onSecondaryFixed 2b4442
onSecondaryFixedVariant 47605e
tertiary 37647b
tertiaryDim 2a586e
onTertiary f4faff
tertiaryContainer b6e3fe
onTertiaryContainer 255369
tertiaryFixed b6e3fe
tertiaryFixedDim a8d5ef
onTertiaryFixed 0b4156
onTertiaryFixedVariant 2f5d73
error a83836
errorDim 67040d
onError fff7f6
errorContainer fa746f
onErrorContainer 6e0a12
primaryPaletteKeyColor 3a827e
secondaryPaletteKeyColor 627c7a
tertiaryPaletteKeyColor 517d94
neutralPaletteKeyColor 737877
neutralVariantPaletteKeyColor 6e7978
errorPaletteKeyColor c84f4c
primary_paletteKeyColor 3a827e
secondary_paletteKeyColor 627c7a
tertiary_paletteKeyColor 517d94
neutral_paletteKeyColor 737877
neutral_variant_paletteKeyColor 6e7978
term0 9a9b99
term1 005bcc
term2 00907c
term3 427d3b
term4 269a7a
term5 0071a3
term6 128f8d
term7 1f2324
term8 0f0f0f
term9 0071fa
term10 00b49c
term11 5d9954
term12 52be9c
term13 008cca
term14 45b0ae
term15 25292a
rosewater 6b8647
flamingo 6f7c1e
pink 0085c0
mauve 005d6c
red 515900
maroon 606c00
peach 198900
yellow 008f67
green 007d6d
teal 007573
sky 00878d
sapphire 008080
blue 00636d
lavender 007e8b
klink 00969d
klinkSelection 00969e
kvisited 008ca9
kvisitedSelection 008ca9
knegative 838f00
knegativeSelection 838f00
kneutral 34c359
kneutralSelection 34c359
kpositive 00beab
kpositiveSelection 00beac
text 2a3433
subtext1 566160
subtext0 727d7c
overlay2 828c8b
overlay1 949d9c
overlay0 a5aead
surface2 b8bfbe
surface1 cbd1d0
surface0 e1e6e5
base f6faf9
mantle eef1f0
crust e9eceb
success 4F6354
onSuccess FFFFFF
successContainer D1E8D5
onSuccessContainer 0C1F13
-2
View File
@@ -2,8 +2,6 @@
# Optimized for smooth and responsive visualization
[general]
# Number of bars (20-200) - fewer bars = better performance
bars = 64
# Framerate (1-144) - higher = smoother but more CPU intensive
framerate = 60
+457
View File
@@ -0,0 +1,457 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"name": "Caelestia",
"author": "Caelestia",
"themes": [
{
"name": "Caelestia",
"appearance": "{{ mode }}",
"style": {
"background": "#{{ surface.hex }}",
"border": "#{{ outlineVariant.hex }}40",
"border.variant": "#{{ outlineVariant.hex }}60",
"border.focused": "#{{ primary.hex }}",
"border.selected": "#{{ primary.hex }}80",
"border.transparent": "#00000000",
"border.disabled": "#{{ outlineVariant.hex }}30",
"elevated_surface.background": "#{{ surfaceContainerHigh.hex }}",
"surface.background": "#{{ surface.hex }}",
"element.background": "#{{ outlineVariant.hex }}40",
"element.hover": "#{{ outlineVariant.hex }}60",
"element.active": "#{{ primary.hex }}30",
"element.selected": "#{{ primary.hex }}20",
"element.disabled": "#{{ outlineVariant.hex }}20",
"drop_target.background": "#{{ primary.hex }}20",
"ghost_element.background": "#00000000",
"ghost_element.hover": "#{{ outlineVariant.hex }}40",
"ghost_element.active": "#{{ primary.hex }}30",
"ghost_element.selected": "#{{ primary.hex }}20",
"ghost_element.disabled": "#{{ outlineVariant.hex }}20",
"text": "#{{ onSurface.hex }}",
"text.muted": "#{{ onSurfaceVariant.hex }}",
"text.placeholder": "#{{ outline.hex }}",
"text.disabled": "#{{ outline.hex }}80",
"text.accent": "#{{ primary.hex }}",
"icon": "#{{ onSurface.hex }}",
"icon.muted": "#{{ onSurfaceVariant.hex }}",
"icon.disabled": "#{{ outlineVariant.hex }}60",
"icon.placeholder": "#{{ onSurfaceVariant.hex }}",
"icon.accent": "#{{ primary.hex }}",
"status_bar.background": "#{{ surface.hex }}",
"title_bar.background": "#{{ surface.hex }}",
"title_bar.inactive_background": "#{{ surface.hex }}",
"toolbar.background": "#{{ surface.hex }}",
"tab_bar.background": "#{{ surface.hex }}",
"tab.inactive_background": "#{{ surface.hex }}",
"tab.active_background": "#{{ surfaceContainerHigh.hex }}",
"search.match_background": "#{{ yellow.hex }}40",
"panel.background": "#{{ surface.hex }}",
"panel.focused_border": "#{{ primary.hex }}",
"pane.focused_border": "#{{ primary.hex }}",
"scrollbar.thumb.background": "#{{ outlineVariant.hex }}30",
"scrollbar.thumb.hover_background": "#{{ outlineVariant.hex }}60",
"scrollbar.thumb.border": "#{{ outlineVariant.hex }}20",
"scrollbar.track.background": "#00000000",
"scrollbar.track.border": "#00000000",
"editor.foreground": "#{{ onSurface.hex }}",
"editor.background": "#{{ surface.hex }}",
"editor.gutter.background": "#{{ surface.hex }}",
"editor.subheader.background": "#{{ surfaceContainer.hex }}",
"editor.active_line.background": "#{{ surfaceContainerHigh.hex }}60",
"editor.highlighted_line.background": "#{{ primary.hex }}15",
"editor.line_number": "#{{ onSurfaceVariant.hex }}",
"editor.active_line_number": "#{{ onSurface.hex }}",
"editor.invisible": "#{{ outlineVariant.hex }}40",
"editor.wrap_guide": "#{{ outlineVariant.hex }}30",
"editor.active_wrap_guide": "#{{ outlineVariant.hex }}60",
"editor.document_highlight.read_background": "#{{ primary.hex }}20",
"editor.document_highlight.write_background": "#{{ primary.hex }}30",
"terminal.background": "#{{ surface.hex }}",
"terminal.foreground": "#{{ onSurface.hex }}",
"terminal.bright_foreground": "#{{ onSurface.hex }}",
"terminal.dim_foreground": "#{{ onSurfaceVariant.hex }}",
"terminal.ansi.black": "#{{ surface.hex }}",
"terminal.ansi.bright_black": "#{{ onSurfaceVariant.hex }}",
"terminal.ansi.dim_black": "#{{ surface.hex }}80",
"terminal.ansi.red": "#{{ red.hex }}",
"terminal.ansi.bright_red": "#{{ maroon.hex }}",
"terminal.ansi.dim_red": "#{{ red.hex }}80",
"terminal.ansi.green": "#{{ green.hex }}",
"terminal.ansi.bright_green": "#{{ teal.hex }}",
"terminal.ansi.dim_green": "#{{ green.hex }}80",
"terminal.ansi.yellow": "#{{ yellow.hex }}",
"terminal.ansi.bright_yellow": "#{{ peach.hex }}",
"terminal.ansi.dim_yellow": "#{{ yellow.hex }}80",
"terminal.ansi.blue": "#{{ blue.hex }}",
"terminal.ansi.bright_blue": "#{{ sapphire.hex }}",
"terminal.ansi.dim_blue": "#{{ blue.hex }}80",
"terminal.ansi.magenta": "#{{ mauve.hex }}",
"terminal.ansi.bright_magenta": "#{{ pink.hex }}",
"terminal.ansi.dim_magenta": "#{{ mauve.hex }}80",
"terminal.ansi.cyan": "#{{ teal.hex }}",
"terminal.ansi.bright_cyan": "#{{ sky.hex }}",
"terminal.ansi.dim_cyan": "#{{ teal.hex }}80",
"terminal.ansi.white": "#{{ onSurface.hex }}",
"terminal.ansi.bright_white": "#{{ onSurface.hex }}",
"terminal.ansi.dim_white": "#{{ onSurface.hex }}80",
"link_text.hover": "#{{ primary.hex }}",
"conflict": "#{{ yellow.hex }}",
"conflict.background": "#{{ yellow.hex }}15",
"conflict.border": "#{{ yellow.hex }}",
"created": "#{{ green.hex }}",
"created.background": "#{{ green.hex }}15",
"created.border": "#{{ green.hex }}",
"deleted": "#{{ red.hex }}",
"deleted.background": "#{{ red.hex }}15",
"deleted.border": "#{{ red.hex }}",
"error": "#{{ error.hex }}",
"error.background": "#{{ error.hex }}15",
"error.border": "#{{ error.hex }}",
"hidden": "#{{ outline.hex }}",
"hidden.background": "#{{ outline.hex }}15",
"hidden.border": "#{{ outline.hex }}",
"hint": "#{{ success.hex }}",
"hint.background": "#{{ success.hex }}15",
"hint.border": "#{{ success.hex }}",
"ignored": "#{{ outline.hex }}",
"ignored.background": "#{{ outline.hex }}15",
"ignored.border": "#{{ outline.hex }}",
"info": "#{{ blue.hex }}",
"info.background": "#{{ blue.hex }}15",
"info.border": "#{{ blue.hex }}",
"modified": "#{{ peach.hex }}",
"modified.background": "#{{ peach.hex }}15",
"modified.border": "#{{ peach.hex }}",
"predictive": "#{{ onSurfaceVariant.hex }}",
"predictive.background": "#{{ onSurfaceVariant.hex }}15",
"predictive.border": "#{{ outlineVariant.hex }}40",
"renamed": "#{{ teal.hex }}",
"renamed.background": "#{{ teal.hex }}15",
"renamed.border": "#{{ teal.hex }}",
"success": "#{{ success.hex }}",
"success.background": "#{{ success.hex }}15",
"success.border": "#{{ success.hex }}",
"unreachable": "#{{ outline.hex }}",
"unreachable.background": "#{{ outline.hex }}15",
"unreachable.border": "#{{ outline.hex }}",
"warning": "#{{ yellow.hex }}",
"warning.background": "#{{ yellow.hex }}15",
"warning.border": "#{{ yellow.hex }}",
"players": [
{
"cursor": "#{{ onSurface.hex }}",
"selection": "#{{ onSurface.hex }}60",
"background": "#{{ primary.hex }}"
},
{
"cursor": "#{{ teal.hex }}",
"selection": "#{{ teal.hex }}40",
"background": "#{{ teal.hex }}"
},
{
"cursor": "#{{ pink.hex }}",
"selection": "#{{ pink.hex }}40",
"background": "#{{ pink.hex }}"
},
{
"cursor": "#{{ yellow.hex }}",
"selection": "#{{ yellow.hex }}40",
"background": "#{{ yellow.hex }}"
},
{
"cursor": "#{{ green.hex }}",
"selection": "#{{ green.hex }}40",
"background": "#{{ green.hex }}"
},
{
"cursor": "#{{ red.hex }}",
"selection": "#{{ red.hex }}40",
"background": "#{{ red.hex }}"
},
{
"cursor": "#{{ blue.hex }}",
"selection": "#{{ blue.hex }}40",
"background": "#{{ blue.hex }}"
},
{
"cursor": "#{{ maroon.hex }}",
"selection": "#{{ maroon.hex }}40",
"background": "#{{ maroon.hex }}"
}
],
"syntax": {
"attribute": {
"color": "#{{ yellow.hex }}",
"font_style": "italic",
"font_weight": null
},
"boolean": {
"color": "#{{ peach.hex }}",
"font_style": null,
"font_weight": null
},
"comment": {
"color": "#{{ subtext0.hex }}",
"font_style": "italic",
"font_weight": null
},
"comment.doc": {
"color": "#{{ subtext0.hex }}",
"font_style": "italic",
"font_weight": null
},
"constant": {
"color": "#{{ peach.hex }}",
"font_style": null,
"font_weight": null
},
"constructor": {
"color": "#{{ yellow.hex }}",
"font_style": null,
"font_weight": null
},
"embedded": {
"color": "#{{ onSurface.hex }}",
"font_style": null,
"font_weight": null
},
"emphasis": {
"color": "#{{ red.hex }}",
"font_style": "italic",
"font_weight": null
},
"emphasis.strong": {
"color": "#{{ red.hex }}",
"font_style": null,
"font_weight": 700
},
"enum": {
"color": "#{{ yellow.hex }}",
"font_style": null,
"font_weight": null
},
"function": {
"color": "#{{ blue.hex }}",
"font_style": null,
"font_weight": null
},
"function.builtin": {
"color": "#{{ teal.hex }}",
"font_style": null,
"font_weight": null
},
"function.definition": {
"color": "#{{ blue.hex }}",
"font_style": null,
"font_weight": null
},
"function.method": {
"color": "#{{ blue.hex }}",
"font_style": null,
"font_weight": null
},
"function.special.definition": {
"color": "#{{ blue.hex }}",
"font_style": null,
"font_weight": null
},
"hint": {
"color": "#{{ onSurfaceVariant.hex }}",
"font_style": "italic",
"font_weight": null
},
"keyword": {
"color": "#{{ pink.hex }}",
"font_style": null,
"font_weight": null
},
"label": {
"color": "#{{ yellow.hex }}",
"font_style": null,
"font_weight": null
},
"link_text": {
"color": "#{{ blue.hex }}",
"font_style": null,
"font_weight": null
},
"link_uri": {
"color": "#{{ teal.hex }}",
"font_style": "underline",
"font_weight": null
},
"number": {
"color": "#{{ peach.hex }}",
"font_style": null,
"font_weight": null
},
"operator": {
"color": "#{{ sapphire.hex }}",
"font_style": null,
"font_weight": null
},
"predictive": {
"color": "#{{ onSurfaceVariant.hex }}",
"font_style": "italic",
"font_weight": null
},
"preproc": {
"color": "#{{ teal.hex }}",
"font_style": null,
"font_weight": null
},
"primary": {
"color": "#{{ onSurface.hex }}",
"font_style": null,
"font_weight": null
},
"property": {
"color": "#{{ teal.hex }}",
"font_style": null,
"font_weight": null
},
"punctuation": {
"color": "#{{ subtext1.hex }}",
"font_style": null,
"font_weight": null
},
"punctuation.bracket": {
"color": "#{{ subtext1.hex }}",
"font_style": null,
"font_weight": null
},
"punctuation.delimiter": {
"color": "#{{ subtext1.hex }}",
"font_style": null,
"font_weight": null
},
"punctuation.list_marker": {
"color": "#{{ teal.hex }}",
"font_style": null,
"font_weight": null
},
"punctuation.special": {
"color": "#{{ sapphire.hex }}",
"font_style": null,
"font_weight": null
},
"string": {
"color": "#{{ green.hex }}",
"font_style": null,
"font_weight": null
},
"string.escape": {
"color": "#{{ pink.hex }}",
"font_style": null,
"font_weight": null
},
"string.regex": {
"color": "#{{ sky.hex }}",
"font_style": null,
"font_weight": null
},
"string.special": {
"color": "#{{ green.hex }}",
"font_style": null,
"font_weight": null
},
"string.special.symbol": {
"color": "#{{ teal.hex }}",
"font_style": null,
"font_weight": null
},
"tag": {
"color": "#{{ yellow.hex }}",
"font_style": null,
"font_weight": null
},
"text.literal": {
"color": "#{{ green.hex }}",
"font_style": null,
"font_weight": null
},
"title": {
"color": "#{{ blue.hex }}",
"font_style": null,
"font_weight": 700
},
"type": {
"color": "#{{ yellow.hex }}",
"font_style": null,
"font_weight": null
},
"type.builtin": {
"color": "#{{ onSurface.hex }}",
"font_style": null,
"font_weight": null
},
"type.interface": {
"color": "#{{ yellow.hex }}",
"font_style": null,
"font_weight": null
},
"type.super": {
"color": "#{{ yellow.hex }}",
"font_style": "italic",
"font_weight": null
},
"variable": {
"color": "#{{ onSurface.hex }}",
"font_style": null,
"font_weight": null
},
"variable.member": {
"color": "#{{ teal.hex }}",
"font_style": null,
"font_weight": null
},
"variable.parameter": {
"color": "#{{ teal.hex }}",
"font_style": "italic",
"font_weight": null
},
"variable.special": {
"color": "#{{ onSurface.hex }}",
"font_style": "italic",
"font_weight": null
},
"variant": {
"color": "#{{ peach.hex }}",
"font_style": null,
"font_weight": null
}
}
}
}
]
}
+1 -1
View File
@@ -6,7 +6,7 @@ from caelestia.utils.scheme import get_scheme_names, scheme_variants
from caelestia.utils.wallpaper import get_wallpaper
def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
def parse_args() -> tuple[argparse.ArgumentParser, argparse.Namespace]:
parser = argparse.ArgumentParser(prog="caelestia", description="Main control script for the Caelestia dotfiles")
parser.add_argument("-v", "--version", action="store_true", help="print the current version")
+2 -1
View File
@@ -7,6 +7,7 @@ from argparse import Namespace
from datetime import datetime
from pathlib import Path
from caelestia.utils import hypr
from caelestia.utils.notify import close_notification, notify
from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir, user_config_path
@@ -36,7 +37,7 @@ class Command:
def start(self) -> None:
args = ["-w"]
monitors = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"]))
monitors = hypr.message("monitors")
if self.args.region:
if self.args.region == "slurp":
region = subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True)
-2
View File
@@ -29,8 +29,6 @@ class Command:
def _load_window_rules(self) -> list[WindowRule]:
default_rules = [
WindowRule("(Bitwarden", "titleContains", "20%", "54%", ["float", "center"]),
WindowRule("Sign in - Google Accounts", "titleContains", "35%", "65%", ["float", "center"]),
WindowRule("oauth", "titleContains", "30%", "60%", ["float", "center"]),
WindowRule("^[Pp]icture(-| )in(-| )[Pp]icture$", "titleRegex", "", "", ["pip"]),
]
+5 -2
View File
@@ -26,8 +26,11 @@ class Command:
else:
sc_data = subprocess.check_output(["grim", "-l", "0", "-g", self.args.region.strip(), "-"])
swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True)
swappy.stdin.write(sc_data)
swappy.stdin.close()
# Ensure stdin is not None for the type checker
if swappy.stdin:
swappy.stdin.write(sc_data)
swappy.stdin.close()
def fullscreen(self) -> None:
sc_data = subprocess.check_output(["grim", "-"])
+7 -4
View File
@@ -33,11 +33,14 @@ class Command:
subprocess.run(args)
else:
shell = subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True)
for line in shell.stdout:
if self.filter_log(line):
print(line, end="")
def shell(self, *args: list[str]) -> str:
# Ensure stdout is not None for the type checker
if shell.stdout:
for line in shell.stdout:
if self.filter_log(line):
print(line, end="")
def shell(self, *args: str) -> str:
return subprocess.check_output(["qs", "-c", "caelestia", *args], text=True)
def filter_log(self, line: str) -> bool:
+16 -12
View File
@@ -3,6 +3,7 @@ import shlex
import shutil
from argparse import Namespace
from collections import ChainMap
from typing import Any, Callable, cast
from caelestia.utils import hypr
from caelestia.utils.paths import user_config_path
@@ -52,8 +53,8 @@ class DeepChainMap(ChainMap):
class Command:
args: Namespace
cfg: dict[str, dict[str, dict[str, any]]] | DeepChainMap
clients: list[dict[str, any]] = None
cfg: dict[str, dict[str, dict[str, Any]]] | DeepChainMap
clients: list[dict[str, Any]] | None = None
def __init__(self, args: Namespace) -> None:
self.args = args
@@ -120,27 +121,27 @@ class Command:
if not spawned:
hypr.dispatch("togglespecialworkspace", self.args.workspace)
def get_clients(self) -> list[dict[str, any]]:
def get_clients(self) -> list[dict[str, Any]]:
if self.clients is None:
self.clients = hypr.message("clients")
self.clients = cast(list[dict[str, Any]], hypr.message("clients"))
return self.clients
def move_client(self, selector: callable, workspace: str) -> None:
def move_client(self, selector: Callable, workspace: str) -> None:
for client in self.get_clients():
if selector(client) and client["workspace"]["name"] != f"special:{workspace}":
hypr.dispatch("movetoworkspacesilent", f"special:{workspace},address:{client['address']}")
def spawn_client(self, selector: callable, spawn: list[str]) -> bool:
def spawn_client(self, selector: Callable, spawn: list[str]) -> bool:
if (spawn[0].endswith(".desktop") or shutil.which(spawn[0])) and not any(
selector(client) for client in self.get_clients()
):
hypr.dispatch("exec", f"[workspace special:{self.args.workspace}] app2unit -- {shlex.join(spawn)}")
return True
return False
else:
return False
def handle_client_config(self, client: dict[str, any]) -> bool:
def selector(c: dict[str, any]) -> bool:
def handle_client_config(self, client: dict[str, Any]) -> bool:
def selector(c: dict[str, Any]) -> bool:
# Each match is or, inside matches is and
for match in client["match"]:
if is_subset(c, match):
@@ -156,5 +157,8 @@ class Command:
return spawned
def specialws(self) -> None:
special = next(m for m in hypr.message("monitors") if m["focused"])["specialWorkspace"]["name"]
hypr.dispatch("togglespecialworkspace", special[8:] or "special")
monitors = cast(list[dict[str, Any]], hypr.message("monitors"))
target = next((m for m in monitors if m.get("focused")), None)
if target:
special = target.get("specialWorkspace", {}).get("name", "")[8:] or "special"
hypr.dispatch("togglespecialworkspace", special)
+2 -3
View File
@@ -11,8 +11,7 @@ def stddev(values: list[float], mean_val: float) -> float:
return math.sqrt(sum((x - mean_val) ** 2 for x in values) / len(values)) if values else 0
def calc_colourfulness(image: Image) -> float:
width, height = image.size
def calc_colourfulness(image: Image.Image) -> float:
pixels = list(image.getdata()) # List of (R, G, B) tuples
rg_diffs = []
@@ -32,7 +31,7 @@ def calc_colourfulness(image: Image) -> float:
return math.sqrt(std_rg**2 + std_yb**2) + 0.3 * math.sqrt(mean_rg**2 + mean_yb**2)
def get_variant(image: Image) -> str:
def get_variant(image: Image.Image) -> str:
colourfulness = calc_colourfulness(image)
if colourfulness < 10:
+14 -10
View File
@@ -1,17 +1,18 @@
import json as j
import json
import os
import socket
from typing import Any
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]:
def message(msg: str, is_json: bool = True) -> str | dict[str, Any]:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(socket_path)
if json:
if is_json:
msg = f"j/{msg}"
sock.send(msg.encode())
@@ -22,14 +23,17 @@ def message(msg: str, json: bool = True) -> str | dict[str, any]:
break
resp += new_resp.decode()
return j.loads(resp) if json else resp
return json.loads(resp) if is_json else resp
def dispatch(dispatcher: str, *args: list[any]) -> bool:
return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok"
def dispatch(dispatcher: str, *args: str) -> bool:
return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), is_json=False) == "ok"
def batch(*msgs: list[str], json: bool = False) -> str | dict[str, any]:
if json:
msgs = (f"j/{m.strip()}" for m in msgs)
return message(f"[[BATCH]]{';'.join(msgs)}", json=False)
def batch(*msgs: str, is_json: bool = False) -> str | dict[str, Any]:
formatted_msgs = msgs
if is_json:
formatted_msgs = [f"j/{m.strip()}" for m in msgs]
return message(f"[[BATCH]]{';'.join(formatted_msgs)}", is_json=False)
+25 -17
View File
@@ -11,6 +11,14 @@ from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant
from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
from typing import Protocol, Any
# The base DynamicScheme class requires a 'variant' argument, but the specific
# subclasses in get_scheme() handle that internally. This Protocol tells the type
# checker to expect our specific 3-argument setup instead of the base class signature.
class SchemeConstructor(Protocol):
def __call__(self, source_color_hct: Any, is_dark: bool, contrast_level: float) -> DynamicScheme: ...
try:
from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme
@@ -147,7 +155,7 @@ def darken(colour: Hct, amount: float) -> Hct:
return Hct.from_hct(colour.hue, colour.chroma - diff / 5, colour.tone - diff)
def get_scheme(scheme: str) -> DynamicScheme:
def get_scheme(scheme: str) -> SchemeConstructor:
if scheme == "content":
return SchemeContent
if scheme == "expressive":
@@ -168,12 +176,12 @@ def get_scheme(scheme: str) -> DynamicScheme:
def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
light = scheme.mode == "light"
is_light = scheme.mode == "light"
colours = {}
# Material colours
primary_scheme = get_scheme(scheme.variant)(primary, not light, 0)
primary_scheme = get_scheme(scheme.variant)(source_color_hct=primary, is_dark=not is_light, contrast_level=0.0)
if hasattr(MaterialDynamicColors, "all_colors"): # materialyoucolor-python >= 3.0.0
dyn_colours = MaterialDynamicColors()
for colour in dyn_colours.all_colors:
@@ -191,28 +199,28 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
colours["neutral_variant_paletteKeyColor"] = colours["neutralVariantPaletteKeyColor"]
# Harmonize terminal colours
for i, hct in enumerate(light_gruvbox if light else dark_gruvbox):
for i, hct in enumerate(light_gruvbox if is_light else dark_gruvbox):
if scheme.variant == "monochrome":
colours[f"term{i}"] = grayscale(hct, light)
colours[f"term{i}"] = grayscale(hct, is_light)
else:
colours[f"term{i}"] = harmonize(
hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if light else 1)
hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if is_light else 1)
)
# Harmonize named colours
for i, hct in enumerate(light_catppuccin if light else dark_catppuccin):
for i, hct in enumerate(light_catppuccin if is_light else dark_catppuccin):
if scheme.variant == "monochrome":
colours[colour_names[i]] = grayscale(hct, light)
colours[colour_names[i]] = grayscale(hct, is_light)
else:
colours[colour_names[i]] = harmonize(hct, colours["primary_paletteKeyColor"], (-0.2 if light else 0.05))
colours[colour_names[i]] = harmonize(hct, colours["primary_paletteKeyColor"], (-0.2 if is_light else 0.05))
# KColours
for colour in kcolours:
colours[colour["name"]] = harmonize(colour["hct"], colours["primary"], 0.1)
colours[f"{colour['name']}Selection"] = harmonize(colour["hct"], colours["onPrimaryFixedVariant"], 0.1)
if scheme.variant == "monochrome":
colours[colour["name"]] = grayscale(colours[colour["name"]], light)
colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], light)
colours[colour["name"]] = grayscale(colours[colour["name"]], is_light)
colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], is_light)
if scheme.variant == "neutral":
for name, hct in colours.items():
@@ -221,8 +229,8 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
# Darken surfaces for hard flavour
if scheme.flavour == "hard":
for colour in "background", *(k for k in colours.keys() if k.startswith("surface")):
colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.8)
colours["term0"] = lighten(colours["term0"], 0.4) if light else darken(colours["term0"], 0.9)
colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.8)
colours["term0"] = lighten(colours["term0"], 0.4) if is_light else darken(colours["term0"], 0.9)
# FIXME: deprecated stuff
colours["text"] = colours["onBackground"]
@@ -241,13 +249,13 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
# More darkening if hard flavour
if scheme.flavour == "hard":
for colour in "base", "mantle", "crust":
colours[colour] = lighten(colours[colour], 0.4) if light else darken(colours[colour], 0.9)
colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.9)
for i in range(3):
colours[f"overlay{i}"] = (
lighten(colours[f"overlay{i}"], 0.4) if light else darken(colours[f"overlay{i}"], 0.8)
lighten(colours[f"overlay{i}"], 0.4) if is_light else darken(colours[f"overlay{i}"], 0.8)
)
colours[f"surface{i}"] = (
lighten(colours[f"surface{i}"], 0.4) if light else darken(colours[f"surface{i}"], 0.8)
lighten(colours[f"surface{i}"], 0.4) if is_light else darken(colours[f"surface{i}"], 0.8)
)
# For debugging
@@ -256,7 +264,7 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
colours = {k: hex(v.to_int())[4:] for k, v in colours.items()}
# Extended material
if light:
if is_light:
colours["success"] = "4F6354"
colours["onSuccess"] = "FFFFFF"
colours["successContainer"] = "D1E8D5"
+1 -1
View File
@@ -1,7 +1,7 @@
import subprocess
def notify(*args: list[str]) -> str:
def notify(*args: str) -> str:
return subprocess.check_output(["notify-send", "-a", "caelestia-cli", *args], text=True).strip()
+30 -29
View File
@@ -4,41 +4,42 @@ import os
import shutil
import tempfile
from pathlib import Path
from typing import Any
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"))
pictures_dir = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures"))
videos_dir = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos"))
config_dir: Path = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
data_dir: Path = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share"))
state_dir: Path = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state"))
cache_dir: Path = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache"))
pictures_dir: Path = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures"))
videos_dir: Path = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos"))
c_config_dir = config_dir / "caelestia"
c_data_dir = data_dir / "caelestia"
c_state_dir = state_dir / "caelestia"
c_cache_dir = cache_dir / "caelestia"
c_config_dir: Path = config_dir / "caelestia"
c_data_dir: Path = data_dir / "caelestia"
c_state_dir: Path = state_dir / "caelestia"
c_cache_dir: Path = cache_dir / "caelestia"
user_config_path = c_config_dir / "cli.json"
cli_data_dir = Path(__file__).parent.parent / "data"
templates_dir = cli_data_dir / "templates"
user_templates_dir = c_config_dir / "templates"
theme_dir = c_state_dir / "theme"
user_config_path: Path = c_config_dir / "cli.json"
cli_data_dir: Path = Path(__file__).parent.parent / "data"
templates_dir: Path = cli_data_dir / "templates"
user_templates_dir: Path = c_config_dir / "templates"
theme_dir: Path = c_state_dir / "theme"
scheme_path = c_state_dir / "scheme.json"
scheme_data_dir = cli_data_dir / "schemes"
scheme_cache_dir = c_cache_dir / "schemes"
scheme_path: Path = c_state_dir / "scheme.json"
scheme_data_dir: Path = cli_data_dir / "schemes"
scheme_cache_dir: Path = c_cache_dir / "schemes"
wallpapers_dir = os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "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"
wallpapers_dir: Path = Path(os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers"))
wallpaper_path_path: Path = c_state_dir / "wallpaper/path.txt"
wallpaper_link_path: Path = c_state_dir / "wallpaper/current"
wallpaper_thumbnail_path: Path = c_state_dir / "wallpaper/thumbnail.jpg"
wallpapers_cache_dir: Path = c_cache_dir / "wallpapers"
screenshots_dir = os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots")
screenshots_cache_dir = c_cache_dir / "screenshots"
screenshots_dir: Path = Path(os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots"))
screenshots_cache_dir: Path = c_cache_dir / "screenshots"
recordings_dir = os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings")
recording_path = c_state_dir / "record/recording.mp4"
recording_notif_path = c_state_dir / "record/notifid.txt"
recordings_dir: Path = Path(os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings"))
recording_path: Path = c_state_dir / "record/recording.mp4"
recording_notif_path: Path = c_state_dir / "record/notifid.txt"
def compute_hash(path: Path | str) -> str:
@@ -51,7 +52,7 @@ def compute_hash(path: Path | str) -> str:
return sha.hexdigest()
def atomic_dump(path: Path, content: dict[str, any]) -> None:
def atomic_dump(path: Path, content: dict[str, Any]) -> None:
with tempfile.NamedTemporaryFile("w") as f:
json.dump(content, f)
f.flush()
+14 -13
View File
@@ -1,6 +1,7 @@
import json
import random
from pathlib import Path
from typing import Any
from caelestia.utils.notify import notify
from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path
@@ -14,19 +15,19 @@ class Scheme:
_colours: dict[str, str]
notify: bool
def __init__(self, json: dict[str, any] | None) -> None:
if json is None:
def __init__(self, scheme_json: dict[str, Any] | None) -> None:
if scheme_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"]
self._name = scheme_json["name"]
self._flavour = scheme_json["flavour"]
self._mode = scheme_json["mode"]
self._variant = scheme_json["variant"]
self._colours = scheme_json["colours"]
self.notify = False
@property
@@ -196,7 +197,7 @@ scheme_variants = [
"content",
]
scheme: Scheme = None
scheme: Scheme | None = None
def read_colours_from_file(path: Path) -> dict[str, str]:
@@ -225,7 +226,7 @@ def get_scheme_names() -> list[str]:
return [*(f.name for f in scheme_data_dir.iterdir() if f.is_dir()), "dynamic"]
def get_scheme_flavours(name: str = None) -> list[str]:
def get_scheme_flavours(name: str | None = None) -> list[str]:
if name is None:
name = get_scheme().name
@@ -234,11 +235,11 @@ def get_scheme_flavours(name: str = None) -> list[str]:
)
def get_scheme_modes(name: str = None, flavour: str = None) -> list[str]:
if name is None:
def get_scheme_modes(name: str | None = None, flavour: str | None = None) -> list[str]:
if name is None or flavour is None:
scheme = get_scheme()
name = scheme.name
flavour = scheme.flavour
name = name or scheme.name
flavour = flavour or scheme.flavour
if name == "dynamic":
return ["light", "dark"]
+103 -32
View File
@@ -1,5 +1,6 @@
import fcntl
import json
import os
import re
import shutil
import subprocess
@@ -17,6 +18,7 @@ from caelestia.utils.paths import (
user_config_path,
user_templates_dir,
)
from caelestia.utils.scheme import get_scheme
def gen_conf(colours: dict[str, str]) -> str:
@@ -34,10 +36,10 @@ def gen_scss(colours: dict[str, str]) -> str:
def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) -> str:
template = template.read_text()
new_template = template.read_text()
for name, colour in colours.items():
template = template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour)
return template
new_template = new_template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour)
return new_template
def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> str:
@@ -65,7 +67,7 @@ def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> s
return template_filled
def c2s(c: str, *i: list[int]) -> str:
def hex_to_ansi(c: str, *i: 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\\"
@@ -82,29 +84,29 @@ def gen_sequences(colours: dict[str, str]) -> str:
16+: 256 colours
"""
return (
c2s(colours["onSurface"], 10)
+ c2s(colours["surface"], 11)
+ c2s(colours["secondary"], 12)
+ c2s(colours["secondary"], 17)
+ c2s(colours["term0"], 4, 0)
+ c2s(colours["term1"], 4, 1)
+ c2s(colours["term2"], 4, 2)
+ c2s(colours["term3"], 4, 3)
+ c2s(colours["term4"], 4, 4)
+ c2s(colours["term5"], 4, 5)
+ c2s(colours["term6"], 4, 6)
+ c2s(colours["term7"], 4, 7)
+ c2s(colours["term8"], 4, 8)
+ c2s(colours["term9"], 4, 9)
+ c2s(colours["term10"], 4, 10)
+ c2s(colours["term11"], 4, 11)
+ c2s(colours["term12"], 4, 12)
+ c2s(colours["term13"], 4, 13)
+ c2s(colours["term14"], 4, 14)
+ c2s(colours["term15"], 4, 15)
+ c2s(colours["primary"], 4, 16)
+ c2s(colours["secondary"], 4, 17)
+ c2s(colours["tertiary"], 4, 18)
hex_to_ansi(colours["onSurface"], 10)
+ hex_to_ansi(colours["surface"], 11)
+ hex_to_ansi(colours["secondary"], 12)
+ hex_to_ansi(colours["secondary"], 17)
+ hex_to_ansi(colours["term0"], 4, 0)
+ hex_to_ansi(colours["term1"], 4, 1)
+ hex_to_ansi(colours["term2"], 4, 2)
+ hex_to_ansi(colours["term3"], 4, 3)
+ hex_to_ansi(colours["term4"], 4, 4)
+ hex_to_ansi(colours["term5"], 4, 5)
+ hex_to_ansi(colours["term6"], 4, 6)
+ hex_to_ansi(colours["term7"], 4, 7)
+ hex_to_ansi(colours["term8"], 4, 8)
+ hex_to_ansi(colours["term9"], 4, 9)
+ hex_to_ansi(colours["term10"], 4, 10)
+ hex_to_ansi(colours["term11"], 4, 11)
+ hex_to_ansi(colours["term12"], 4, 12)
+ hex_to_ansi(colours["term13"], 4, 13)
+ hex_to_ansi(colours["term14"], 4, 14)
+ hex_to_ansi(colours["term15"], 4, 15)
+ hex_to_ansi(colours["primary"], 4, 16)
+ hex_to_ansi(colours["secondary"], 4, 17)
+ hex_to_ansi(colours["tertiary"], 4, 18)
)
@@ -302,7 +304,7 @@ def _determine_hue_color(r: int, g: int, b: int, brightness: int, use_pale: bool
@log_exception
def apply_gtk(colours: dict[str, str], mode: str) -> None:
def apply_gtk(colours: dict[str, str], mode: str, icon_theme: str | None = None) -> None:
gtk_template = gen_replace(colours, templates_dir / "gtk.css", hash=True)
thunar_template = gen_replace(colours, templates_dir / "thunar.css", hash=True)
@@ -313,18 +315,21 @@ def apply_gtk(colours: dict[str, str], mode: str) -> None:
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/gtk-theme", "'adw-gtk3-dark'"])
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'prefer-{mode}'"])
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'Papirus-{mode.capitalize()}'"])
gtk_icon_theme = icon_theme if icon_theme is not None else f"Papirus-{mode.capitalize()}"
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'{gtk_icon_theme}'"])
sync_papirus_colors(colours["primary"])
@log_exception
def apply_qt(colours: dict[str, str], mode: str) -> None:
def apply_qt(colours: dict[str, str], mode: str, icon_theme: str | None = None) -> None:
colours = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True)
write_file(config_dir / "qtengine/caelestia.colors", colours)
config = (templates_dir / "qtengine.json").read_text()
config = config.replace("{{ $mode }}", mode.capitalize())
if icon_theme is not None:
config = config.replace(f'"iconTheme": "Papirus-{mode.capitalize()}"', f'"iconTheme": "{icon_theme}"')
write_file(config_dir / "qtengine/config.json", config)
@@ -337,6 +342,51 @@ def apply_warp(colours: dict[str, str], mode: str) -> None:
write_file(data_dir / "warp-terminal/themes/caelestia.yaml", template)
@log_exception
def apply_chromium(colours: dict[str, str]) -> None:
surface_hex = colours["surface"]
theme_color = f"#{surface_hex}"
browsers = [
("chromium", Path("/etc/chromium/policies/managed")),
("brave", Path("/etc/brave/policies/managed")),
("google-chrome-stable", Path("/etc/opt/chrome/policies/managed")),
]
for cmd, policy_dir in browsers:
if shutil.which(cmd) is None:
continue
if not policy_dir.is_dir():
subprocess.run(["sudo", "-n", "mkdir", "-p", str(policy_dir)], stderr=subprocess.DEVNULL)
if not policy_dir.is_dir():
print(f"Unable to create {policy_dir} directory")
continue
# Use tee instead of write_file cause we need sudo
subprocess.run(
["sudo", "-n", "tee", str(policy_dir / "caelestia.json")],
input=json.dumps({"BrowserThemeColor": theme_color, "BrowserColorScheme": "device"}),
text=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
[cmd, "--refresh-platform-policy", "--no-startup-window"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def apply_zed(colours: dict[str, str], mode: str) -> None:
theme_path = config_dir / "zed/themes/caelestia.json"
# Zed's file watcher does not detect changes through symlinks,
# so resolve to a regular file before writing
if theme_path.is_symlink():
theme_path.unlink()
content = gen_replace_dynamic(colours, templates_dir / "zed.json", mode)
write_file(theme_path, content)
@log_exception
def apply_cava(colours: dict[str, str]) -> None:
template = gen_replace(colours, templates_dir / "cava.conf", hash=True)
@@ -393,16 +443,37 @@ def apply_colours(colours: dict[str, str], mode: str) -> None:
apply_nvtop(colours)
if check("enableHtop"):
apply_htop(colours)
icon_theme = cfg.get(f"iconTheme{mode.capitalize()}") or cfg.get("iconTheme")
if check("enableGtk"):
apply_gtk(colours, mode)
apply_gtk(colours, mode, icon_theme)
if check("enableQt"):
apply_qt(colours, mode)
apply_qt(colours, mode, icon_theme)
if check("enableWarp"):
apply_warp(colours, mode)
if check("enableChromium"):
apply_chromium(colours)
if check("enableZed"):
apply_zed(colours, mode)
if check("enableCava"):
apply_cava(colours)
apply_user_templates(colours, mode)
if post_hook := cfg.get("postHook"):
scheme = get_scheme()
subprocess.run(
post_hook,
shell=True,
env={
**os.environ,
"SCHEME_NAME": scheme.name,
"SCHEME_FLAVOUR": scheme.flavour,
"SCHEME_MODE": scheme.mode,
"SCHEME_VARIANT": scheme.variant,
"SCHEME_COLOURS": json.dumps(scheme.colours),
},
stderr=subprocess.DEVNULL,
)
finally:
try:
lock_file.unlink()
+26 -13
View File
@@ -2,8 +2,10 @@ import json
import os
import random
import subprocess
from argparse import Namespace
from pathlib import Path
from typing import cast
from materialyoucolor.hct import Hct
from materialyoucolor.utils.color_utils import argb_from_rgb
@@ -11,6 +13,7 @@ from PIL import Image
from caelestia.utils.hypr import message
from caelestia.utils.material import get_colours_for_image
from caelestia.utils.colourfulness import get_variant
from caelestia.utils.paths import (
compute_hash,
user_config_path,
@@ -33,7 +36,7 @@ def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bo
return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold
def get_wallpaper() -> str:
def get_wallpaper() -> str | None:
try:
return wallpaper_path_path.read_text()
except IOError:
@@ -41,16 +44,16 @@ def get_wallpaper() -> str:
def get_wallpapers(args: Namespace) -> list[Path]:
dir = Path(args.random)
if not dir.is_dir():
directory = Path(args.random)
if not directory.is_dir():
return []
walls = [f for f in dir.rglob("*") if is_valid_image(f)]
walls = [f for f in directory.rglob("*") if is_valid_image(f)]
if args.no_filter:
return walls
monitors = message("monitors")
monitors = cast(list[dict[str, int]], message("monitors"))
filter_size = min(m["width"] for m in monitors), min(m["height"] for m in monitors)
return [f for f in walls if check_wall(f, filter_size, args.threshold)]
@@ -62,14 +65,14 @@ def get_thumb(wall: Path, cache: Path) -> Path:
if not thumb.exists():
with Image.open(wall) as img:
img = img.convert("RGB")
img.thumbnail((128, 128), Image.NEAREST)
img.thumbnail((128, 128), Image.Resampling.NEAREST)
thumb.parent.mkdir(parents=True, exist_ok=True)
img.save(thumb, "JPEG")
return thumb
def get_smart_opts(wall: Path, cache: Path) -> str:
def get_smart_opts(wall: Path, cache: Path) -> dict:
opts_cache = cache / "smart.json"
try:
@@ -77,15 +80,16 @@ def get_smart_opts(wall: Path, cache: Path) -> str:
except (IOError, json.JSONDecodeError):
pass
from caelestia.utils.colourfulness import get_variant
opts = {}
with Image.open(get_thumb(wall, cache)) as img:
opts["variant"] = get_variant(img)
img.thumbnail((1, 1), Image.Resampling.LANCZOS)
# Cast the pixel to a tuple of 3 integers to safely unpack it
pixel = cast(tuple[int, int, int], img.getpixel((0, 0)))
hct = Hct.from_int(argb_from_rgb(*pixel))
img.thumbnail((1, 1), Image.LANCZOS)
hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0))))
opts["mode"] = "light" if hct.tone > 60 else "dark"
opts_cache.parent.mkdir(parents=True, exist_ok=True)
@@ -144,7 +148,7 @@ def convert_gif(wall: Path) -> Path:
return output_path
def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
def set_wallpaper(wall: Path, no_smart: bool) -> None:
# Make path absolute
wall = Path(wall).resolve()
@@ -188,7 +192,16 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
subprocess.run(
post_hook,
shell=True,
env={**os.environ, "WALLPAPER_PATH": str(wall)},
env={
**os.environ,
"WALLPAPER_PATH": str(wall),
"SCHEME_NAME": scheme.name,
"SCHEME_FLAVOUR": scheme.flavour,
"SCHEME_MODE": scheme.mode,
"SCHEME_VARIANT": scheme.variant,
"SCHEME_COLOURS": json.dumps(scheme.colours),
"THUMBNAIL_PATH": str(thumb),
},
stderr=subprocess.DEVNULL,
)
except (FileNotFoundError, json.JSONDecodeError):