253 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
2 * r + 2 * t c930bd2604 feat: switch to qtengine 2026-03-09 21:36:44 +11:00
2 * r + 2 * t cc155cf432 fix: format 2026-03-09 21:26:37 +11:00
github-actions 6e59149fbf [CI] chore: update flake 2026-03-09 02:52:41 +00:00
github-actions 8d2b737f15 [CI] chore: update flake 2026-03-08 02:51:20 +00:00
github-actions 4bcd42f482 [CI] chore: update flake 2026-03-06 02:36:22 +00:00
xeisenberg 51cecd481c fix: wall not Path type (#89) 2026-03-04 19:30:11 +11:00
github-actions c9312f3928 [CI] chore: update flake 2026-03-04 02:36:02 +00:00
github-actions bfaf4fc373 [CI] chore: update flake 2026-03-03 02:43:42 +00:00
2 * r + 2 * t 6e711ec289 fix: dynamic scheme import <3.0.0 compat 2026-03-03 01:04:33 +11:00
github-actions 7899f8348f [CI] chore: update flake 2026-03-01 03:00:32 +00:00
github-actions b0d68f0a1c [CI] chore: update flake 2026-02-25 02:51:37 +00:00
github-actions b0325a1898 [CI] chore: update flake 2026-02-22 02:46:26 +00:00
github-actions a6defd2921 [CI] chore: update flake 2026-02-21 02:33:51 +00:00
2 * r + 2 * t 0b9e416175 fix: missing colon 2026-02-21 00:52:12 +11:00
Unrectified 8ce97ea3f5 feat: add GIF files support as wallpaper (#88)
* feat: add GIF files support as wallpaper (not animated tho)

* tweak: simplifying variable use

* fix: git is good but damn it's annoying

* fix

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2026-02-21 00:17:16 +11:00
github-actions 25c473c18e [CI] chore: update flake 2026-02-20 02:40:44 +00:00
github-actions c22916fe45 [CI] chore: update flake 2026-02-19 02:45:57 +00:00
Soramane 011989e3ca fix: dynamic scheme import 2026-02-18 11:18:15 +11:00
Nathachou d88cc7ff79 feat: scheme support for Pandora Minecraft launcher (#87)
* added pandora theme template json

* feat: add support for Pandora theme integration
2026-02-17 11:36:42 +11:00
github-actions a550eb79ed [CI] chore: update flake 2026-02-15 02:56:44 +00:00
2 * r + 2 * t d890f7c3af feat: add dynamic hard flavour
Someone requested idk who tho
2026-02-15 00:24:14 +11:00
Robin Seger bca7b12072 feat: thunar & papirus-folders theming + new schemes (#80)
* feat: GTK app theming system

- Implemented custom.css import for user-managed app themes
- process_app_themes() to dynamically update colors in imported CSS files
- Inline comment markers for color replacement (e.g, /* accent-color */)
- Papirus icon color syncing with weighted hue/saturation algorithm

This allows users to create modular app themes that automatically update when the scheme/wallpaper changes

Example usage:
  .app .element { color: #24BD5C; /* accent-color */ }
  .app .element:hover { background: rgba(36, 189, 92, 0.15); /* accent-color with 15% opacity */ }

* feat: atomic theme changes with locking and mode-specific CSS

- Implemented locking to prevent concurrent theme changes
- Added mode-light/mode-dark CSS markers for dynamic property reordering
- Made terminal writes and Papirus sync non-blocking to prevent hangs
- Only save scheme.json after successful theme application

Fixes race conditions during rapid theme switching and ensures Shell and GTK apps scheme stay in sync.

* theme: added to color mapping for custom theming, new schemes

* theme: quick fixes, cleanup

* theme: include thunar.css as template, with new theming system

* theme: modified GTK theming approach

- Dropped comment targeted theming in favor for existing {{  }} replacement
- [app].css.template file created for customization, bypassing built in default if present
- Handling *.template for added templates to be parsed and added to import

* theme: fixes for  thunar.css

* theme: remove .template file use

* theme: path button color adjustment, non-active hover

* fixes & cleanup

* thunar css fixes

* more css fixes

* format

* fix tab vert spacing

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2026-02-14 23:23:33 +11:00
2 * r + 2 * t fe8adde6c1 fix: compat for python-materialyoucolor < 3.0.0 2026-02-14 23:22:30 +11:00
github-actions 9c309473f4 [CI] chore: update flake 2026-02-13 02:50:31 +00:00
github-actions 6e5ab6db53 [CI] chore: update flake 2026-02-11 02:54:26 +00:00
Nick 170405fbb0 nix: fix todoist command replacement in default.nix (#86)
Target the command list specifically ["todoist"] instead of a global
replace to avoid breaking the configuration key structure.
2026-02-10 22:02:18 +11:00
AteebXYZ 6a91c7b990 fix: apply mode substitution after dynamic template fill (#84) 2026-02-10 21:37:58 +11:00
2 * r + 2 * t bdc5f91009 fix: xxx_paletteKeyColor rename in myc 3.0.0
Fixes caelestia-dots/shell#1112
2026-02-10 21:33:19 +11:00
Chea Vuthearith 164bda5462 record: add arg to copy screen recording to clipboard (#83) 2026-02-10 20:53:27 +11:00
github-actions fe071cb285 [CI] chore: update flake 2026-02-09 02:54:39 +00:00
github-actions 2d5bec14a5 [CI] chore: update flake 2026-02-08 03:19:32 +00:00
github-actions 2395347d36 [CI] chore: update flake 2026-02-06 02:39:29 +00:00
github-actions cf6092b77b [CI] chore: update flake 2026-02-05 02:40:10 +00:00
github-actions 6dad795297 [CI] chore: update flake 2026-02-04 02:38:06 +00:00
github-actions 016a566bb7 [CI] chore: update flake 2026-02-03 02:42:43 +00:00
github-actions 2240ad4adf [CI] chore: update flake 2026-02-02 02:53:33 +00:00
github-actions fccf32250e [CI] chore: update flake 2026-02-01 03:04:44 +00:00
github-actions 90fc2a981e [CI] chore: update flake 2026-01-30 02:37:13 +00:00
github-actions 6cdb131e3a [CI] chore: update flake 2026-01-26 02:35:07 +00:00
github-actions ecf0c6156a [CI] chore: update flake 2026-01-25 02:35:40 +00:00
Kalagmitan 52a3a3c50e theme: inject mode into user templates (#77)
* theme: apply_user_templates accepts mode.

* Some themes like those in NvChad require a mode to be supplied to
work. Added a minimal change that makes apply_user_templates accept a
mode parameter and replaces any {{ mode }} placeholder in a file with
the actual mode.

* theme: mode replace integrated with gen_replace_dynamic

+ Moved the {{ mode }} replacement logic to the gen_replace_dynamic
function.

* refactor: adjusted comment
2026-01-24 14:45:32 +11:00
github-actions 6b3f927d2c [CI] chore: update flake 2026-01-23 02:11:56 +00:00
github-actions 74ddac98eb [CI] chore: update flake 2026-01-22 02:21:06 +00:00
github-actions d09e36299a [CI] chore: update flake 2026-01-21 02:21:30 +00:00
github-actions 50c8f9c84d [CI] chore: update flake 2026-01-20 02:12:10 +00:00
github-actions 33fb69dc8c [CI] chore: update flake 2026-01-18 02:32:10 +00:00
mj0x0 7de6c60631 theme: ensure atomic writes for configuration files (#76)
* fix: ensure atomic writes for configuration files

* requested changes
2026-01-18 00:11:13 +11:00
github-actions 0cd11ef665 [CI] chore: update flake 2026-01-17 02:06:07 +00:00
github-actions 64686104a1 [CI] chore: update flake 2026-01-16 02:12:09 +00:00
2 * r + 2 * t 08dc3e0e87 theme: update discord theme template
Fixes caelestia-dots/caelestia#206
2026-01-14 19:22:19 +11:00
github-actions 7900d05459 [CI] chore: update flake 2026-01-13 02:08:26 +00:00
github-actions 315f1daf9d [CI] chore: update flake 2026-01-12 02:23:10 +00:00
github-actions ad555d5129 [CI] chore: update flake 2026-01-11 02:32:22 +00:00
github-actions 70a8624eac [CI] chore: update flake 2026-01-10 02:06:38 +00:00
github-actions 86b0c94bd1 [CI] chore: update flake 2026-01-08 02:10:20 +00:00
github-actions 55d75a1175 [CI] chore: update flake 2026-01-07 02:09:52 +00:00
github-actions 55590bd9e4 [CI] chore: update flake 2026-01-04 02:27:36 +00:00
github-actions 337c711371 [CI] chore: update flake 2026-01-02 02:16:30 +00:00
github-actions f3ea42d43a [CI] chore: update flake 2025-12-29 02:28:26 +00:00
github-actions e33e6ccf72 [CI] chore: update flake 2025-12-28 02:30:51 +00:00
github-actions b049cb1749 [CI] chore: update flake 2025-12-26 02:06:29 +00:00
github-actions 78e0b9d795 [CI] chore: update flake 2025-12-23 02:07:06 +00:00
github-actions e9f3f00bb9 [CI] chore: update flake 2025-12-21 02:13:41 +00:00
github-actions 8c83ae1e6a [CI] chore: update flake 2025-12-20 01:59:18 +00:00
github-actions 10639b5de0 [CI] chore: update flake 2025-12-16 02:07:04 +00:00
github-actions e1efe59e55 [CI] chore: update flake 2025-12-14 02:13:56 +00:00
github-actions b1e4e92e98 [CI] chore: update flake 2025-12-10 02:04:42 +00:00
github-actions 3e59c5acc2 [CI] chore: update flake 2025-12-07 02:18:43 +00:00
github-actions 1eead86151 [CI] chore: update flake 2025-12-04 02:02:53 +00:00
github-actions 02442e0f85 [CI] chore: update flake 2025-12-03 02:02:50 +00:00
github-actions 5fa4d1b278 [CI] chore: update flake 2025-12-02 02:03:08 +00:00
github-actions c0813790c2 [CI] chore: update flake 2025-11-30 02:17:28 +00:00
github-actions ed12d4cb82 [CI] chore: update flake 2025-11-29 01:56:50 +00:00
github-actions 1e1edfcd38 [CI] chore: update flake 2025-11-27 01:58:53 +00:00
github-actions 0fdab31bc7 [CI] chore: update flake 2025-11-26 02:07:00 +00:00
github-actions 909c25eefd [CI] chore: update flake 2025-11-25 02:06:40 +00:00
github-actions 434841e000 [CI] chore: update flake 2025-11-23 02:19:03 +00:00
github-actions 178e73a065 [CI] emojis: update data 2025-11-23 01:39:22 +00:00
github-actions 1cfd405eaa [CI] chore: update flake 2025-11-19 01:58:19 +00:00
github-actions 03d485ec07 [CI] chore: update flake 2025-11-18 01:58:41 +00:00
github-actions 0c08188584 [CI] chore: update flake 2025-11-16 02:11:44 +00:00
github-actions d89c438284 [CI] chore: update flake 2025-11-15 02:01:51 +00:00
github-actions f9acac8fb2 [CI] chore: update flake 2025-11-13 02:00:29 +00:00
github-actions 1e3d75fa8f [CI] chore: update flake 2025-11-10 02:04:22 +00:00
github-actions 5a80ac77ed [CI] chore: update flake 2025-11-09 02:02:36 +00:00
github-actions 9323fbf1b4 [CI] chore: update flake 2025-11-07 01:58:39 +00:00
github-actions c05f6ef57a [CI] chore: update flake 2025-11-05 01:59:44 +00:00
Evence Wang 57dfea955e shell: fix log rules arg (#67) 2025-11-04 23:13:00 +11:00
github-actions 0ae99bbe39 [CI] chore: update flake 2025-11-04 01:58:32 +00:00
github-actions f13803af6b [CI] chore: update flake 2025-11-02 02:09:23 +00:00
github-actions 1cea6fb42a [CI] chore: update flake 2025-11-01 02:01:08 +00:00
github-actions 94d5477908 [CI] chore: update flake 2025-10-30 02:00:53 +00:00
github-actions a8590220bb [CI] chore: update flake 2025-10-27 02:05:30 +00:00
github-actions 27be3dbd84 [CI] chore: update flake 2025-10-26 02:01:08 +00:00
2 * r + 2 * t cc458bf859 scheme: ignore empty lines in schemes 2025-10-25 22:41:34 +11:00
github-actions b2ea4e3aad [CI] chore: update flake 2025-10-24 01:50:36 +00:00
github-actions 98da4c0266 [CI] chore: update flake 2025-10-23 02:00:03 +00:00
github-actions a9f8bde28e [CI] chore: update flake 2025-10-22 01:58:45 +00:00
github-actions d0f8a06e59 [CI] chore: update flake 2025-10-20 02:07:40 +00:00
github-actions dcca082ee6 [CI] chore: update flake 2025-10-19 02:10:10 +00:00
Davi Ribeiro 4593b823d7 wallpaper: fix random for multi-monitor setup (#63) 2025-10-17 04:59:13 +11:00
Givani Boekestijn c6f46db36c feat: add wallpaper post-hook for dynamic theming (#61)
* feat: add wallpaper post-hook for dynamic theming

Adds support for running custom shell commands after wallpaper changes
via the `wallpaper.postHook` config option in `~/.config/caelestia/cli.json`.

The wallpaper path is made available to the hook via the $WALLPAPER_PATH
environment variable. This enables integration with tools like `matugen`
or `pywal` for dynamic theming based on wallpaper colors.

The hook runs after apply_colours() in set_wallpaper(), ensuring it
executes for all wallpaper change methods (UI, IPC, and direct CLI).

* Replaced comment in example config with no-op command
2025-10-17 04:56:32 +11:00
github-actions 40fbf3bd68 [CI] chore: update flake 2025-10-16 01:53:51 +00:00
github-actions 8dc745c936 [CI] chore: update flake 2025-10-15 01:54:51 +00:00
github-actions 14ee3c66ef [CI] chore: update flake 2025-10-14 01:52:02 +00:00
github-actions 601d08bdec [CI] chore: update flake 2025-10-12 02:00:34 +00:00
github-actions e9668d58f5 [CI] chore: update flake 2025-10-11 01:45:56 +00:00
github-actions 3f7f3bab15 [CI] chore: update flake 2025-10-09 01:51:33 +00:00
github-actions 1394e32a7e [CI] chore: update flake 2025-10-08 01:55:44 +00:00
Davi Ribeiro f912d33f26 nix: fix toggle app2unit (#60) 2025-10-07 15:14:35 +11:00
github-actions 8b1d0257df [CI] chore: update flake 2025-10-07 01:49:44 +00:00
github-actions 276f18d198 [CI] chore: update flake 2025-10-05 02:04:47 +00:00
github-actions ebbd636b79 [CI] chore: update flake 2025-10-04 01:45:05 +00:00
github-actions ee7c7c2d5d [CI] chore: update flake 2025-10-03 01:49:13 +00:00
github-actions 28a831779e [CI] chore: update flake 2025-10-02 01:49:11 +00:00
github-actions 069d3a95da [CI] chore: update flake 2025-09-30 01:47:52 +00:00
github-actions af79030bf5 [CI] chore: update flake 2025-09-28 02:00:02 +00:00
github-actions 1de7da5f2b [CI] chore: update flake 2025-09-27 01:46:12 +00:00
github-actions 4be834aa11 [CI] chore: update flake 2025-09-26 01:51:20 +00:00
github-actions 8399eee947 [CI] chore: update flake 2025-09-25 01:51:43 +00:00
github-actions 62e5dc317a [CI] chore: update flake 2025-09-24 01:50:54 +00:00
github-actions 60a6be2dfd [CI] chore: update flake 2025-09-23 01:49:21 +00:00
github-actions 7fc5bcca1a [CI] chore: update flake 2025-09-22 01:59:18 +00:00
github-actions 64fd3a4a28 [CI] chore: update flake 2025-09-21 02:02:29 +00:00
github-actions dd982bcb96 [CI] chore: update flake 2025-09-20 01:46:38 +00:00
github-actions 1bac394029 [CI] chore: update flake 2025-09-19 01:51:52 +00:00
github-actions b36794bbb5 [CI] chore: update flake 2025-09-18 01:48:31 +00:00
github-actions c3e8a6de72 [CI] chore: update flake 2025-09-17 01:48:57 +00:00
github-actions 65df31dca4 [CI] chore: update flake 2025-09-16 01:48:54 +00:00
2 * r + 2 * t ad6df1c9d2 record: add extraArgs config 2025-09-15 23:18:08 +10:00
github-actions e038c5d86a [CI] chore: update flake 2025-09-15 01:57:49 +00:00
2 * r + 2 * t e560a6e3d2 version: catch correct error 2025-09-14 20:45:57 +10:00
2 * r + 2 * t 62e7911864 version: fix pacman + use shell version helper 2025-09-14 00:12:42 +10:00
2 * r + 2 * t 6f8e5849cb readme: update installation section
Add stable/unstable packages + nix
2025-09-14 00:03:22 +10:00
2 * r + 2 * t 54f7611437 ci: create release 2025-09-13 23:36:01 +10:00
2 * r + 2 * t 2eda287a80 record: wl-screenrec -> gpu-screen-recorder
Supports NVIDIA, so no need for having a fallback
Also supports pausing
2025-09-13 22:58:57 +10:00
github-actions 4263e5f809 [CI] chore: update flake 2025-09-13 01:43:07 +00:00
github-actions 70ce21f798 [CI] chore: update flake 2025-09-12 01:50:38 +00:00
github-actions 3f57cd71d1 [CI] chore: update flake 2025-09-11 01:51:24 +00:00
github-actions ad962cb572 [CI] chore: update flake 2025-09-10 01:48:37 +00:00
sweenu 3319d2ca19 theme: continue execution after failure for one theme (#50) 2025-09-09 13:59:04 +10:00
hoangbaoa c20bc567a4 resizer/pip: account for monitor scale (#51) 2025-09-08 23:32:30 +10:00
github-actions d7b7d2ae04 [CI] chore: update flake 2025-09-07 01:59:27 +00:00
github-actions 12abcf2336 [CI] chore: update flake 2025-09-06 01:47:03 +00:00
github-actions d6c1e13246 [CI] chore: update flake 2025-09-05 01:50:33 +00:00
github-actions 597780ba78 [CI] chore: update flake 2025-09-04 01:47:51 +00:00
github-actions 47730a22b9 [CI] chore: update flake 2025-09-03 01:47:08 +00:00
github-actions f4fee9c3d5 [CI] chore: update flake 2025-09-02 01:55:09 +00:00
github-actions 67942d1d7a [CI] chore: update flake 2025-09-01 02:08:56 +00:00
2 * r + 2 * t fc09d2fcd3 ci: fix update-emojis 2025-08-31 14:47:15 +10:00
github-actions 351ebb60c6 [CI] chore: update flake 2025-08-31 02:01:49 +00:00
2 * r + 2 * t 8bc7e495af dev: better direnv
Override caelestia command with dev version instead of using ./run.sh
Allows completions to work
2025-08-30 22:19:33 +10:00
Matheus Oliveira 35b10394b6 record: fix wf-recorder audio flag and proc error handling (#48)
* fix(recording): Fix wf-recorder audio flag and improve process monitoring

- Fix incorrect audio flag format for wf-recorder(Invalid whitespace)
  Changed from `-a <device>` to `--audio=<device>` as per wf-recorder docs:
  "Specify device like this: -a<device> or --audio=<device>"

- Add fallback to IDLE audio sources
  Audio sources are typically in IDLE state when no media is playing.
  Now falls back to IDLE sources if no RUNNING sources are found,
  ensuring audio capture works when recording starts during silence
  but media plays later.

- Improve process startup monitoring
  The 0.1s sleep was insufficient for reliable process detection on NVIDIA systems.
  Process would start and immediately die ~90% of the time when triggered via keybinds.
  Now shows immediate UI feedback then monitors for 3 seconds to ensure
  stable process startup while maintaining responsive user experience.

* check returncode + timeout 3s -> 1s + format

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2025-08-30 21:59:26 +10:00
github-actions 46a9516f72 [CI] chore: update flake 2025-08-30 01:47:27 +00:00
github-actions 7467089ebe [CI] chore: update flake 2025-08-29 01:53:00 +00:00
github-actions eb18d29056 [CI] chore: update flake 2025-08-28 01:53:14 +00:00
github-actions 59d1bb532a [CI] chore: update flake 2025-08-27 01:57:12 +00:00
github-actions a9ab4a02d5 [CI] chore: update flake 2025-08-26 01:57:21 +00:00
github-actions 683172ed65 [CI] chore: update flake 2025-08-25 02:01:00 +00:00
github-actions 4782799b55 [CI] chore: update flake 2025-08-24 02:06:56 +00:00
2 * r + 2 * t db1e0da5bb Revert "shell: set QT_QPA_PLATFORMTHEME env"
This reverts commit 99142f11ad.
2025-08-23 22:31:18 +10:00
2 * r + 2 * t 6cdfe72e8b wallpaper: random prevent duplicate 2025-08-23 16:22:09 +10:00
github-actions 78be122d0c [CI] chore: update flake 2025-08-23 01:52:01 +00:00
github-actions 838749ef0a [CI] chore: update flake 2025-08-22 01:56:10 +00:00
github-actions 3802bccc6f [CI] chore: update flake 2025-08-21 01:55:22 +00:00
Matheus Oliveira 12f0d51862 record: use correct proc (#47)
Previously, the start() function always invoked 'wl-screenrec' regardless of detected GPU, which prevented recording on systems with NVIDIA GPUswhen 'wf-recorder' is available.

This change updates start() to launch the recorder stored in
self.recorder, ensuring the correct recorder is used based on GPU detection.
2025-08-20 12:19:06 +10:00
github-actions 202f687dde [CI] chore: update flake 2025-08-20 01:56:46 +00:00
2 * r + 2 * t 99142f11ad shell: set QT_QPA_PLATFORMTHEME env
Hopefully fixes most people's icon issues

Fixes caelestia-dots/shell@390
2025-08-19 17:15:36 +10:00
2 * r + 2 * t f2a9bf2490 ci: fix update flake action
Also update readme help
2025-08-19 15:42:32 +10:00
Batuhan Edgüer c72223a7e6 feat: window resizer command & daemon (#43)
* resizer: add window resizer daemon command

Implements a continuous window resizer daemon that automatically resizes
windows based on configurable rules. Features include:

- Listens to Hyprland socket events for real-time window detection
- Supports multiple match types: initial_title, title_contains, title_exact
- Configurable via CLI config file with fallback to sensible defaults
- Rate limiting to prevent excessive resize operations
- Window actions: float, center, and custom dimensions
- Integration with existing CLI structure

Usage: caelestia resizer --daemon

* refactor: replace pip daemon with integrated resizer functionality

## Summary
- Remove standalone pip daemon and integrate its functionality into the resizer
- Add regex matching, config support, and active mode to resizer
- Implement clean 'caelestia resizer pip' command for quick PiP operations
- Update keybinds to use new unified resizer command

## Why Replace the Old PiP Method?

### 1. Code Duplication
The old pip daemon duplicated window management logic that already existed in the resizer:
- Both daemons listened to Hyprland socket events
- Both had similar window detection and manipulation code
- Both needed rate limiting and error handling

### 2. Limited Functionality
The old pip daemon was restricted:
- Only worked with regex pattern matching for 'Picture in Picture' titles
- No configuration support for custom rules
- No way to apply PiP to arbitrary windows
- No integration with other window actions

### 3. Maintenance Overhead
Having two separate daemons created maintenance issues:
- Two different codebases to maintain and debug
- Potential conflicts when both daemons run simultaneously
- Inconsistent error handling and logging approaches

### 4. Review Feedback Implementation
The PR review specifically requested this consolidation:
- "This can actually probably replace the pip daemon entirely"
- "consider adding a regex match mode and pip action, then add that to the default rules"

## New Integrated Approach Benefits

### 1. Unified Window Management
- Single daemon handles all window operations (resize, float, center, pip)
- Consistent configuration format using camelCase
- Shared error handling and rate limiting

### 2. Enhanced PiP Functionality
- Works with any window title pattern (regex, contains, exact)
- Configurable through CLI config file
- Active mode: `caelestia resizer pip` for quick PiP on current window
- Better error messages and user guidance

### 3. Future-Proof Architecture
- Easy to add new window actions (e.g., minimize, maximize, workspace move)
- Extensible pattern matching (could add class-based matching)
- Single place to implement new Hyprland features

### 4. Improved User Experience
- Simpler command structure: `caelestia resizer pip` vs complex arguments
- Better error messages when windows aren't floating
- Consistent CLI interface across all window operations

## Implementation Details
- Added pip action to WindowRule system
- Integrated original pip calculation with minimum size constraints
- Added type safety improvements throughout
- Maintained backward compatibility for existing users
- Updated keybind: `bind = $kbWindowPip, exec, caelestia resizer pip`

* fix: unpack dispatch_commands list in hypr.batch call

- Fix 'sequence item 0: expected str instance, list found' error
- hypr.batch() expects individual string arguments, not a list
- Use *dispatch_commands to unpack the list properly

* fix: handle Hyprland event format with triple > separators

- Fix window ID parsing for events with >>> instead of >>
- Add .lstrip('>') to remove any leading > characters
- Support both >> and >>> formats for compatibility
- Fixes 'Invalid window ID format: >555ee935ba30' errors

* resizer: implement active mode for all matching windows

Active mode now searches through all open windows and applies the rule to
any that match the specified pattern, rather than just checking if the
currently active window matches. This allows for batch operations on
multiple windows with the same pattern.

Special case: using pattern "active" will still target only the currently
active window, allowing users to apply rules to just the focused window
when needed.

This addresses the latest review feedback requesting that active mode work
on any open window that matches the given pattern.

* parser: better resizer help

* completions: add for resizer

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2025-08-18 17:39:35 +10:00
Batuhan Edgüer 3e19fd6919 theme: add nvtop, htop, and cava support (#45)
* templates: add nvtop, htop, and cava support

* Triggers htop and cava theme reloads

Sends a USR2 signal to htop and cava after their themes are applied. This prompts the applications to reload their configuration files, ensuring new themes are visible instantly without requiring a manual restart.
2025-08-18 17:18:26 +10:00
github-actions febcc5662a [CI] chore: update flake 2025-08-18 02:11:43 +00:00
2 * r + 2 * t f280b9cbb6 schemes: add dark green scheme
Scheme by @depcustodian with some tweaks
2025-08-17 15:25:12 +10:00
2 * r + 2 * t b22ab08a37 pip: use batch request 2025-08-17 15:02:12 +10:00
github-actions ebca5f8557 [CI] chore: update flake 2025-08-17 02:11:15 +00:00
Batuhan Edgüer 63e2132830 theme: add Warp terminal theme support (#42)
* theme: add Warp terminal theme support

Add support for Warp terminal theming with proper template and integration.
- Add warp.yaml template with standard theme variables
- Implement apply_warp function with correct 'darker'/'lighter' values
- Integrate with main theme application pipeline via enableWarp config

* warp: improve theme generation and use proper data directory

- Use gen_replace with hash=True for consistent color formatting
- Remove # symbols from template to avoid double hashes
- Replace warp_mode manually after gen_replace instead of adding to colors dict
- Use data_dir for XDG-compliant theme location
2025-08-17 02:17:39 +10:00
2 * r + 2 * t 1cd8cae2d9 paths: fix custom path envs
Specify whole path instead of end
2025-08-16 18:07:29 +10:00
2 * r + 2 * t e325129f7a record: fix region 2025-08-16 18:05:33 +10:00
Batuhan Edgüer 651efcd137 record: add NVIDIA GPU support with wf-recorder (#41)
* record: add NVIDIA GPU support with wf-recorder

- Add automatic GPU detection to choose between wl-screenrec and wf-recorder
- Use wf-recorder for NVIDIA GPUs to fix compatibility issues
- Map wf-recorder arguments correctly for region, output, and audio recording
- Update documentation to include wf-recorder as dependency for NVIDIA users

Fixes #37

* format + deduplicate

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2025-08-16 18:05:02 +10:00
2 * r + 2 * t 0df89887a0 toggle: improvements
Closes #40
2025-08-16 17:41:23 +10:00
2 * r + 2 * t e24656da0e internal: add ruff format settings 2025-08-16 16:59:59 +10:00
2 * r + 2 * t c9c1be183a toggle: fix specialws 2025-08-16 16:13:02 +10:00
github-actions 6023a37064 [CI] chore: update flake 2025-08-16 01:59:38 +00:00
2 * r + 2 * t d3881bfc26 data: add new colours
NOTE: clear your scheme cache when updating to this version
`rm -r ~/.cache/caelestia/schemes`
2025-08-15 16:00:37 +10:00
2 * r + 2 * t d727836cc9 theme: better qt theming
Switch to Darkly for default QT style
Use qt5ct-kde and qt6ct-kde
2025-08-15 15:53:39 +10:00
github-actions 5b34ef0061 [CI] chore: update flake 2025-08-15 02:05:23 +00:00
github-actions c3631cd35b [CI] chore: update flake 2025-08-14 02:08:02 +00:00
2 * r + 2 * t 386ccf3729 paths: allow configuring via env vars
Closes #39
2025-08-13 16:10:40 +10:00
anders130 1fcfb83fba record: fix multi-monitor and moving across filesystems (#38)
* fix(record): support differing filesystems for recording destination

* fix(record): for multi-monitor-systems wl-screenrec needs a -o argument

* fix(record): replace path.rename with shutil.move

* fix(record): use json option to retrieve hyprland focused monitor

* use generator

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2025-08-13 16:07:12 +10:00
github-actions d15d5c4399 [CI] chore: update flake 2025-08-13 02:07:42 +00:00
2 * r + 2 * t caf26e7c5b ci: fix 2025-08-12 20:31:27 +10:00
Soramane ff38a8c5cf ci: update flake inputs daily
cause shell updates frequently
2025-08-09 13:50:22 +10:00
Soramane 9489f0d4f6 nix: use nixpkgs app2unit
update flake inputs
2025-08-09 12:59:36 +10:00
2 * r + 2 * t 7027ea5442 ci: only check specific file 2025-08-09 12:48:19 +10:00
2 * r + 2 * t f541e99d07 pip: add monitor offset
Fixes #30 (hopefully)
2025-08-04 17:48:58 +10:00
2 * r + 2 * t ff6ca32b11 readme: add config section 2025-08-04 17:39:32 +10:00
2 * r + 2 * t ae8deb35a7 toggle: allow configuring
Closes #33
2025-08-04 17:33:43 +10:00
2 * r + 2 * t 50646cd565 paths: use xdg user paths
Closes #35
2025-08-04 16:48:15 +10:00
2 * r + 2 * t fed8cc5800 theme: add config for progs to theme 2025-08-04 16:43:55 +10:00
github-actions 84e16c9968 [CI] chore: update flake 2025-08-04 06:13:47 +00:00
2 * r + 2 * t 1d5ba89573 ci: screw it 2025-08-04 16:08:08 +10:00
2 * r + 2 * t b4ea0f6db6 ci: fix (hopefully?) 2025-08-04 15:58:16 +10:00
2 * r + 2 * t 981f686a3c nix: add with shell package
Also fix ci
2025-08-04 15:48:31 +10:00
2 * r + 2 * t 43fb0cfc35 ci: dont test random scheme
Cause it can die when no wall
2025-08-04 15:43:32 +10:00
2 * r + 2 * t 46e05afc56 ci: run emoji update weekly 2025-08-04 15:41:24 +10:00
2 * r + 2 * t d8037819f0 ci: add flake update workflow
Also add contributing, funding and issue templates

parser: add kill option to shell
version: fix errors when not on arch
2025-08-04 15:40:17 +10:00
Elio Torquet 06a7102490 theme: add template system (#36)
* user template system

* fix when templates dir doesnt exist

Also color -> colour

---------

Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
2025-08-04 15:14:10 +10:00
Soramane 2bde2ddfbf shell: fix log when no log rules
also fix nix devshell
2025-07-25 11:41:58 +10:00
2 * r + 2 * t 882adb2c6c record: don't use hevc codec
Cause incompatible with some players (mainly discord)
2025-07-23 18:29:28 +10:00
2 * r + 2 * t 13a2d46d08 shell: remove default log rules
The spammy logs have been silenced
2025-07-22 19:12:28 +10:00
2 * r + 2 * t beabe2683c completions: silence when no shell running 2025-07-22 16:05:03 +10:00
2 * r + 2 * t 3deb726278 theme: ignore perm errors for /dev/pts
Fixes #27
2025-07-22 16:01:12 +10:00
Soramane 465c200c83 nix: fix circular dep
update flake inputs
2025-07-17 13:09:07 +10:00
79 changed files with 5002 additions and 499 deletions
+5 -1
View File
@@ -1 +1,5 @@
use flake
if has nix; then
use flake
fi
PATH_add bin
+20
View File
@@ -0,0 +1,20 @@
# Contributing
There are only a few rules:
- Follow the commit convention as follows:
- The name of the commit should be `module: change`
- Try to be consistent with the module names; you can look at existing commits for the module names I use
- If there is more than one change, the change in the commit name should be the most impactful change
- Put other changes in the description
- Format your code
- Just try to follow the code style of the rest of the code and ensure that there is:
- no trailing whitespace on any lines
- a single space between operators
- No AI slop allowed
- AI readme/docs slop = instant block
- PLEASE TEST YOUR PRS
- I can't believe I have to put this here, but please test your PRs before submitting them
- Your PR must not break anything currently existing, or specify in the description if it does
- PR descriptions should be descriptive
- Please explain what the PR does and how to use it in your PR description
- Also include any breaking changes and/or side effects of the PR
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: soramanew
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: soramane
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: soramane
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+24
View File
@@ -0,0 +1,24 @@
name: Feature request
description: Suggest a new feature
labels: ["enhancement"]
type: "Feature"
title: "[FEATURE] "
body:
- type: markdown
attributes:
value: "NOTE: Please write in **English**."
- type: textarea
attributes:
label: "What would you like to be added?"
description: "Can be a suggestion for an existing feature. You can suggest a widget, minor user interaction changes.. whatever."
- type: textarea
attributes:
label: "How will it help?"
description: "It's helpful to include examples (like in your use case)."
- type: textarea
attributes:
label: "Extra info"
description: "If you want a new widget, a pic of the inspiration (if available) would be awesome."
+56
View File
@@ -0,0 +1,56 @@
name: Issue
description: Report an issue with the dots
labels: ["bug"]
type: "Bug"
title: "[BUG] "
body:
- type: markdown
attributes:
value: "**Welcome to submit a new issue!**\n- It takes only 3 steps, so please be patient :)\n- Tip: If your issue is not a feature request and is not an issue with the dots (e.g. \"how do I use X feature\"), please use [Discussions](https://github.com/caelestia-dots/shell/discussions) instead."
- type: checkboxes
attributes:
label: "Step 1. Before you submit"
description: "Hint: The 2nd and 3rd checkbox is **not** forcely required as you may have failed to do so."
options:
- label: I have read the above instructions and am sure that this is supposed to be posted here.
required: true
- label: I've successfully updated to the latest versions following the [updating guide](https://github.com/caelestia-dots/caelestia?tab=readme-ov-file#updating).
required: false # Not required cuz user may have failed to do so
- label: I've successfully updated the system packages to the latest.
required: false # Not required cuz user may have failed to do so
- label: I've ticked the checkboxes without reading their contents
required: false # Obviously
- type: textarea
attributes:
label: "Step 2. Version info"
description: "Run `caelestia -v` and paste the result below."
value: "<details><summary>Version info</summary>\n\n```\n<!-- Run `caelestia -v` and paste the result here! -->\n```\n\n</details>"
validations:
required: true
- type: markdown
attributes:
value: |
**Tips for the following Step 3**
1. Use `LANG=C LC_ALL=C` to get the output of a command in English, eg. `LANG=C LC_ALL=C date` displays time in English.
2. If it throws errors, **PLEASE**, attach logs and describe in detail if possible.
- The CLI failed to run? Simply post the output below.
- Installation failed? Run installation again for logs.
- You may use more code blocks when needed.
3. In case you are confused, the `<details>`, `<summary>`, `</summary>`, `</details>` are HTML tags for folding the logs (typically very long) inside. Please do not touch them (unless you know what you are doing).
4. If the logs are suuuuuuper long, consider using an online pastebin service instead.
- type: textarea
attributes:
label: "Step 3. Describe the issue"
value: "\n<!-- Firsly describe your issue here! -->\n\n<details><summary>Logs</summary>\n\n```\n<!-- Put your log content here!-->\n```\n\n</details>"
validations:
required: true
- type: checkboxes
attributes:
label: Reminder
options:
- label: I agree that it's usually impossible for others to help me without my logs.
required: true
+38
View File
@@ -0,0 +1,38 @@
name: Create release
on:
push:
tags:
- "v*"
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Clean stale artifacts
run: git clean -dfx
- name: Setup python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install build
run: |
python -m pip install --upgrade pip
pip install build
- name: Create packages
run: python -m build
- name: Create release
uses: softprops/action-gh-release@v2
with:
files: dist/*
generate_release_notes: true
+4 -4
View File
@@ -3,9 +3,9 @@
name: Update emojis
on:
push:
workflow_dispatch:
schedule:
- cron: "0 0 * * 0"
jobs:
update:
@@ -28,11 +28,11 @@ jobs:
pip install .
- name: Fetch emojis
run: ./run.sh emoji -f
run: ./bin/caelestia emoji -f
- name: Check for changes
id: check
run: echo modified=$(test -n "$(git status --porcelain)" && echo 'true' || echo 'false') >> $GITHUB_OUTPUT
run: echo modified=$(git diff --exit-code src/caelestia/data/emojis.txt &>/dev/null && echo 'false' || echo 'true') >> $GITHUB_OUTPUT
- name: Commit and push changes
if: steps.check.outputs.modified == 'true'
+111
View File
@@ -0,0 +1,111 @@
name: Update flake inputs
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
update-flake:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Install Nix
uses: nixbuild/nix-quick-install-action@v31
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- name: Restore and save Nix store
uses: nix-community/cache-nix-action@v6
with:
# restore and save a cache using this key
primary-key: nix-${{ hashFiles('**/*.nix', '**/flake.lock') }}
# if there's no cache hit, restore a cache by this prefix
restore-prefixes-first-match: nix-
# collect garbage until the Nix store size (in bytes) is at most this number
# before trying to save a new cache
# 1G = 1073741824
gc-max-store-size-linux: 1G
# do purge caches
purge: true
# purge all versions of the cache
purge-prefixes: nix-
# created more than this number of seconds ago
purge-created: 0
# or, last accessed more than this number of seconds ago
# relative to the start of the `Post Restore and save Nix store` phase
purge-last-accessed: 0
# except any version with the key that is the same as the `primary-key`
purge-primary-key: never
- name: Update flake inputs
run: nix flake update
- name: Attempt to build flake
run: nix build '.#with-shell'
- name: Test modules
run: |
result/bin/caelestia -v
result/bin/caelestia -h
result/bin/caelestia toggle -h
result/bin/caelestia scheme -h
result/bin/caelestia scheme list
result/bin/caelestia scheme get
result/bin/caelestia scheme set -n gruvbox -f hard -m dark -v content
result/bin/caelestia screenshot -h
result/bin/caelestia record -h
result/bin/caelestia clipboard -h
result/bin/caelestia emoji -h
result/bin/caelestia emoji
result/bin/caelestia wallpaper -h
result/bin/caelestia resizer -h
- name: Test graphical stuff
env:
XDG_RUNTIME_DIR: /home/runner/runtime
WLR_BACKENDS: headless
WLR_LIBINPUT_NO_DEVICES: 1
WAYLAND_DISPLAY: wayland-1
GTK_USE_PORTAL: 0
run: |
mkdir $XDG_RUNTIME_DIR
chown $USER $XDG_RUNTIME_DIR
chmod 0700 $XDG_RUNTIME_DIR
nix profile install 'nixpkgs#sway'
sway &
sleep 3 # Give Sway some time to start
result/bin/caelestia shell -d
sleep 3 # Give the shell some time to start (and die)
# Test CLI graphical modules
result/bin/caelestia clipboard &
result/bin/caelestia emoji -p &
result/bin/caelestia shell -s
result/bin/caelestia shell drawers list
result/bin/caelestia shell mpris list
result/bin/caelestia shell notifs clear
pgrep .quickshell-wra # Fail job if shell died
result/bin/caelestia shell -k
killall sway # Screw using IPC
- name: Check for changes
id: check
run: echo modified=$(git diff --exit-code flake.lock &>/dev/null && echo 'false' || echo 'true') >> $GITHUB_OUTPUT
- name: Commit and push changes
if: steps.check.outputs.modified == 'true'
uses: EndBug/add-and-commit@v9
with:
add: flake.lock
default_author: github_actions
message: "[CI] chore: update flake"
+205 -31
View File
@@ -4,35 +4,68 @@ 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
- [`wl-screenrec`](https://github.com/russelltg/wl-screenrec) - screen recording
- `glib2` - closing notifications
- `libpulse` - getting audio device
- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history
- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
- [`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>
## Installation
### Package manager (recommended)
### Arch linux
The cli is available from the AUR as `caelestia-cli-git`. To install it you can use
an AUR helper like [`yay`](https://github.com/Jguer/yay), or manually download the
PKGBUILD and run `makepkg -si`.
The CLI is available from the AUR as `caelestia-cli`. You can install it with an AUR helper
like [`yay`](https://github.com/Jguer/yay) or manually downloading the PKGBUILD and running `makepkg -si`.
e.g. using yay
A package following the latest commit also exists as `caelestia-cli-git`. This is bleeding edge
and likely to be unstable/have bugs. Regular users are recommended to use the stable package
(`caelestia-cli`).
### Nix
You can run the CLI directly via `nix run`:
```sh
yay -S caelestia-cli-git
nix run github:caelestia-dots/cli
```
Or add it to your system configuration:
```nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
caelestia-cli = {
url = "github:caelestia-dots/cli";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
```
The package is available as `caelestia-cli.packages.<system>.default`, which can be added to your
`environment.systemPackages`, `users.users.<username>.packages`, `home.packages` if using home-manager,
or a devshell. The CLI can then be used via the `caelestia` command.
> [!TIP]
> The default package does not have the shell enabled by default, which is required for full functionality.
> To enable the shell, use the `with-shell` package. This is the recommended installation method, as
> the CLI exposes the shell via the `shell` subcommand, meaning there is no need for the shell package
> to be exposed.
For home-manager, you can also use the Caelestia's home manager module (explained in
[configuring](https://github.com/caelestia-dots/shell?tab=readme-ov-file#home-manager-module)) that
installs and configures the shell and the CLI.
### Manual installation
Install all [dependencies](#dependencies), then install
@@ -44,7 +77,7 @@ Install all [dependencies](#dependencies), then install
e.g. via an AUR helper (yay)
```sh
yay -S libnotify swappy grim dart-sass app2unit wl-clipboard slurp wl-screenrec glib2 libpulse cliphist fuzzel python-build python-installer python-hatch python-hatch-vcs
yay -S libnotify swappy grim dart-sass app2unit wl-clipboard slurp gpu-screen-recorder glib2 cliphist fuzzel python-build python-installer python-hatch python-hatch-vcs
```
Now, clone the repo, `cd` into it, build the wheel via `python -m build --wheel`
@@ -60,30 +93,171 @@ 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.
```
$ caelestia -h
usage: caelestia [-h] COMMAND ...
usage: caelestia [-h] [-v] COMMAND ...
Main control script for the Caelestia dotfiles
options:
-h, --help show this help message and exit
-h, --help show this help message and exit
-v, --version print the current version
subcommands:
valid subcommands
COMMAND the subcommand to run
shell start or message the shell
toggle toggle a special workspace
scheme manage the colour scheme
screenshot take a screenshot
record start a screen recording
clipboard open clipboard history
emoji emoji/glyph utilities
wallpaper manage the wallpaper
pip picture in picture utilities
COMMAND the subcommand to run
shell start or message the shell
toggle toggle a special workspace
scheme manage the colour scheme
screenshot take a screenshot
record start a screen recording
clipboard open clipboard history
emoji emoji/glyph utilities
wallpaper manage the wallpaper
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`.
<details><summary>Example configuration</summary>
```json
{
"record": {
"extraArgs": []
},
"wallpaper": {
"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,
"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": {
"discord": {
"enable": true,
"match": [{ "class": "discord" }],
"command": ["discord"],
"move": true
},
"whatsapp": {
"enable": true,
"match": [{ "class": "whatsapp" }],
"move": true
}
},
"music": {
"spotify": {
"enable": true,
"match": [{ "class": "Spotify" }, { "initialTitle": "Spotify" }, { "initialTitle": "Spotify Free" }],
"command": ["spicetify", "watch", "-s"],
"move": true
},
"feishin": {
"enable": true,
"match": [{ "class": "feishin" }],
"move": true
}
},
"sysmon": {
"btop": {
"enable": true,
"match": [{ "class": "btop", "title": "btop", "workspace": { "name": "special:sysmon" } }],
"command": ["foot", "-a", "btop", "-T", "btop", "fish", "-C", "exec btop"]
}
},
"todo": {
"todoist": {
"enable": true,
"match": [{ "class": "Todoist" }],
"command": ["todoist"],
"move": true
}
}
}
}
```
</details>
+1 -1
View File
@@ -2,6 +2,6 @@
# Utility script for running caelestia
cd "$(dirname $0)/src" || exit
cd "$(dirname $0)/../src" || exit
python -m caelestia "$@"
+8 -5
View File
@@ -1,7 +1,7 @@
set -l seen '__fish_seen_subcommand_from'
set -l has_opt '__fish_contains_opt'
set -l commands shell toggle scheme screenshot record clipboard emoji-picker wallpaper pip
set -l commands shell toggle scheme screenshot record clipboard emoji-picker wallpaper resizer
set -l not_seen "not $seen $commands"
# Disable file completions
@@ -19,7 +19,7 @@ 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 'emoji' -d 'Emoji/glyph utilities'
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'
complete -c caelestia -n $not_seen -a 'resizer' -d 'Window resizer'
# Shell
set -l commands mpris drawers wallpaper notifs
@@ -58,7 +58,7 @@ set -l not_seen "$seen shell && $seen drawers && not $seen $commands"
complete -c caelestia -n $not_seen -a 'list' -d 'List togglable drawers'
complete -c caelestia -n $not_seen -a 'toggle' -d 'Toggle a drawer'
set -l commands (caelestia shell drawers list)
set -l commands (caelestia shell drawers list 2> /dev/null)
complete -c caelestia -n "$seen shell && $seen drawers && $seen toggle && not $seen $commands" -a "$commands" -d 'drawer'
set -l commands list get set
@@ -105,6 +105,7 @@ complete -c caelestia -n "$seen screenshot" -s 'f' -l 'freeze' -d 'Freeze while
# Record
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'
complete -c caelestia -n "$seen record" -s 'c' -l 'clipboard' -d 'Copy recording path to clipboard'
# Clipboard
complete -c caelestia -n "$seen clipboard" -s 'd' -l 'delete' -d 'Delete from cliboard history'
@@ -121,5 +122,7 @@ complete -c caelestia -n "$seen wallpaper" -s 'N' -l 'no-smart' -d 'Disable smar
complete -c caelestia -n "$seen emoji" -s 'p' -l 'picker' -d 'Open emoji/glyph picker'
complete -c caelestia -n "$seen emoji" -s 'f' -l 'fetch' -d 'Fetch emoji/glyph data from remote'
# Pip
complete -c caelestia -n "$seen pip" -s 'd' -l 'daemon' -d 'Start in daemon mode'
# Resizer
complete -c caelestia -n "$seen resizer" -s 'd' -l 'daemon' -d 'Start in daemon mode'
complete -c caelestia -n "$seen resizer" -a 'pip' -d 'Quick pip mode'
complete -c caelestia -n "$seen resizer" -a 'active' -d 'Select the active window'
+9 -8
View File
@@ -12,13 +12,13 @@
dart-sass,
grim,
fuzzel,
wl-screenrec,
gpu-screen-recorder,
dconf,
killall,
caelestia-shell,
withShell ? false,
discordBin ? "discord",
qtctStyle ? "Fusion",
qtctStyle ? "Darkly",
}:
python3.pkgs.buildPythonApplication {
pname = "caelestia-cli";
@@ -50,7 +50,7 @@ python3.pkgs.buildPythonApplication {
dart-sass
grim
fuzzel
wl-screenrec
gpu-screen-recorder
dconf
killall
]
@@ -65,14 +65,15 @@ python3.pkgs.buildPythonApplication {
substituteInPlace src/caelestia/subcommands/screenshot.py \
--replace-fail '"qs", "-c", "caelestia"' '"caelestia-shell"'
# Use config bin instead of discord + fix todoist
# Use config bin instead of discord + fix todoist + fix app2unit
substituteInPlace src/caelestia/subcommands/toggle.py \
--replace-fail 'discord' ${discordBin} \
--replace-fail 'todoist' 'todoist.desktop'
--replace-fail '["todoist"]' '["todoist.desktop"]'\
--replace-fail 'app2unit' ${app2unit}/bin/app2unit
# Use config style instead of fusion
substituteInPlace src/caelestia/data/templates/qtct.conf \
--replace-fail 'Fusion' '${qtctStyle}'
# Use config style instead of darkly
substituteInPlace src/caelestia/data/templates/qtengine.json \
--replace-fail 'Darkly' '${qtctStyle}'
'';
postInstall = "installShellCompletion completions/caelestia.fish";
Generated
+11 -60
View File
@@ -1,67 +1,19 @@
{
"nodes": {
"app2unit": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1752557494,
"narHash": "sha256-GIcH+k321WBUl//gBypaJkLRMrKDemcSpzADJoyUdec=",
"owner": "soramanew",
"repo": "app2unit",
"rev": "574d764446997e30218a29a6b9871fb1b9c6554d",
"type": "github"
},
"original": {
"owner": "soramanew",
"repo": "app2unit",
"type": "github"
}
},
"caelestia-cli": {
"inputs": {
"app2unit": [
"caelestia-shell",
"app2unit"
],
"nixpkgs": [
"caelestia-shell",
"nixpkgs"
]
},
"locked": {
"lastModified": 1752566000,
"narHash": "sha256-xaSDZXvZtuM+88PsmfTDWv6+VxN5cOsT/5/czsk3xgI=",
"owner": "caelestia-dots",
"repo": "cli",
"rev": "b1019d11924d1bc9440f457ddf94fc0d8a230ff4",
"type": "github"
},
"original": {
"owner": "caelestia-dots",
"repo": "cli",
"type": "github"
}
},
"caelestia-shell": {
"inputs": {
"app2unit": [
"app2unit"
],
"caelestia-cli": "caelestia-cli",
"caelestia-cli": [],
"nixpkgs": [
"nixpkgs"
],
"quickshell": "quickshell"
},
"locked": {
"lastModified": 1752637099,
"narHash": "sha256-08oPnEGYkuU7Vqa4F7rOi4E9j2Drigm3DxdOA+/mgF4=",
"lastModified": 1777688289,
"narHash": "sha256-2EaEVkT1oUpjLLp7uEY/hDYDOa2k5R1YgcJpHei+lUM=",
"owner": "caelestia-dots",
"repo": "shell",
"rev": "19431534c954f763eb095dd131fd0b19ff74837b",
"rev": "4e9e1f4b723f7e3a87cb280d67a25ee92c87fbff",
"type": "github"
},
"original": {
@@ -72,11 +24,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1752480373,
"narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
"lastModified": 1777268161,
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
"type": "github"
},
"original": {
@@ -94,11 +46,11 @@
]
},
"locked": {
"lastModified": 1752631407,
"narHash": "sha256-dLDtKxh1VabwLxv5xbjI+oRkDyqWEKGITU+0dEaaW28=",
"lastModified": 1777341401,
"narHash": "sha256-QEAVYeXxvTamsYJVBq8+qSJV9ml2MxqRaZvkobfuPWA=",
"ref": "refs/heads/master",
"rev": "4d8055f1cd9924bcace59405894b8879633eb83d",
"revCount": 638,
"rev": "0baa81aa03559ca315668e5a306364cddf1a6f49",
"revCount": 812,
"type": "git",
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
},
@@ -109,7 +61,6 @@
},
"root": {
"inputs": {
"app2unit": "app2unit",
"caelestia-shell": "caelestia-shell",
"nixpkgs": "nixpkgs"
}
+3 -13
View File
@@ -4,15 +4,10 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
app2unit = {
url = "github:soramanew/app2unit";
inputs.nixpkgs.follows = "nixpkgs";
};
caelestia-shell = {
url = "github:caelestia-dots/shell";
inputs.nixpkgs.follows = "nixpkgs";
inputs.app2unit.follows = "app2unit";
inputs.caelestia-cli.follows = "";
};
};
@@ -31,20 +26,15 @@
packages = forAllSystems (pkgs: rec {
caelestia-cli = pkgs.callPackage ./default.nix {
rev = self.rev or self.dirtyRev;
app2unit = inputs.app2unit.packages.${pkgs.system}.default;
caelestia-shell = inputs.caelestia-shell.packages.${pkgs.system}.default;
};
with-shell = caelestia-cli.override {withShell = true;};
default = caelestia-cli;
});
devShells = forAllSystems (pkgs: {
default = pkgs.mkShellNoCC {
inputsFrom = [self.packages.${pkgs.system}.caelestia-cli];
packages = [
(pkgs.writeShellScriptBin "caelestia" ''
cd src && python -m caelestia "$@"
'')
];
packages = [self.packages.${pkgs.system}.with-shell];
};
});
};
+10
View File
@@ -16,3 +16,13 @@ caelestia = "caelestia:main"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.targets.sdist]
only-include = [
"src",
"completions",
"README.md"
]
[tool.ruff]
line-length = 120
+38 -30
View File
@@ -132,6 +132,7 @@
😯 hushed face epic face hushed omg stunned surprised whoa woah
😲 :o :O astonished face astonished cost face no omg shocked totally way
😳 :$ flushed face amazed awkward crazy dazed dead disbelief embarrassed face flushed geez heat hot impressed jeez what wow
🫪 distorted face anxiety bloated panic shocked surprised vulnerable
🥺 pleading face begging big eyes face mercy not pleading please pretty puppy sad why
🥹 face holding back tears admiration aww back cry embarrassed face feelings grateful gratitude holding joy please proud resist sad tears
😦 frowning face with open mouth caught face frown frowning guard mouth open scared scary surprise what wow
@@ -205,6 +206,7 @@
💋 kiss mark dating emotion heart kiss kissing lips mark romance sexy
💯 hundred points 100 a+ agree clearly definitely faithful fleek full hundred keep perfect point score true truth yup
💢 anger symbol anger angry comic mad symbol upset
🫯 fight cloud argument brawl debate disagreement fight ruckus wrestle
💥 collision bomb boom collide comic explode
💫 dizzy comic shining shooting star stars
💦 sweat droplets comic drip droplet droplets drops splashing squirt sweat water wet work workout
@@ -449,6 +451,7 @@
🧟‍♂️ man zombie apocalypse dead halloween horror man scary undead walking zombie
🧟‍♀️ woman zombie apocalypse dead halloween horror scary undead walking woman zombie
🧌 troll fairy fantasy monster tale trolling
🫈 hairy creature bigfoot cryptid forest giant hairy sasquatch woodwose yeti
💆 person getting massage face getting headache massage person relax relaxing salon soothe spa tension therapy treatment
💆‍♂️ man getting massage face getting headache man massage relax relaxing salon soothe spa tension therapy treatment
💆‍♀️ woman getting massage face getting headache massage relax relaxing salon soothe spa tension therapy treatment woman
@@ -458,42 +461,43 @@
🚶 person walking amble gait hike man pace pedestrian person stride stroll walk walking
🚶‍♂️ man walking amble gait hike man pace pedestrian stride stroll walk walking
🚶‍♀️ woman walking amble gait hike man pace pedestrian stride stroll walk walking woman
🚶‍➡️ person walking facing right amble gait hike man pace pedestrian person stride stroll walk walking
🚶‍♀️‍➡️ woman walking facing right amble gait hike man pace pedestrian stride stroll walk walking woman
🚶‍♂️‍➡️ man walking facing right amble gait hike man pace pedestrian stride stroll walk walking
🚶‍➡️ person walking: facing right amble facing gait hike man pace pedestrian person right stride stroll walk walking
🚶‍♀️‍➡️ woman walking: facing right amble facing gait hike man pace pedestrian right stride stroll walk walking woman
🚶‍♂️‍➡️ man walking: facing right amble facing gait hike man pace pedestrian right stride stroll walk walking
🧍 person standing person stand standing
🧍‍♂️ man standing man stand standing
🧍‍♀️ woman standing stand standing woman
🧎 person kneeling kneel kneeling knees person
🧎‍♂️ man kneeling kneel kneeling knees man
🧎‍♀️ woman kneeling kneel kneeling knees woman
🧎‍➡️ person kneeling facing right kneel kneeling knees person
🧎‍♀️‍➡️ woman kneeling facing right kneel kneeling knees woman
🧎‍♂️‍➡️ man kneeling facing right kneel kneeling knees man
🧎‍➡️ person kneeling: facing right facing kneel kneeling knees person right
🧎‍♀️‍➡️ woman kneeling: facing right facing kneel kneeling knees right woman
🧎‍♂️‍➡️ man kneeling: facing right facing kneel kneeling knees man right
🧑‍🦯 person with white cane accessibility blind cane person probing white
🧑‍🦯‍➡️ person with white cane facing right accessibility blind cane person probing white
🧑‍🦯‍➡️ person with white cane: facing right accessibility blind cane facing person probing right white
👨‍🦯 man with white cane accessibility blind cane man probing white
👨‍🦯‍➡️ man with white cane facing right accessibility blind cane man probing white
👨‍🦯‍➡️ man with white cane: facing right accessibility blind cane facing man probing right white
👩‍🦯 woman with white cane accessibility blind cane probing white woman
👩‍🦯‍➡️ woman with white cane facing right accessibility blind cane probing white woman
👩‍🦯‍➡️ woman with white cane: facing right accessibility blind cane facing probing right white woman
🧑‍🦼 person in motorized wheelchair accessibility motorized person wheelchair
🧑‍🦼‍➡️ person in motorized wheelchair facing right accessibility motorized person wheelchair
🧑‍🦼‍➡️ person in motorized wheelchair: facing right accessibility facing motorized person right wheelchair
👨‍🦼 man in motorized wheelchair accessibility man motorized wheelchair
👨‍🦼‍➡️ man in motorized wheelchair facing right accessibility man motorized wheelchair
👨‍🦼‍➡️ man in motorized wheelchair: facing right accessibility facing man motorized right wheelchair
👩‍🦼 woman in motorized wheelchair accessibility motorized wheelchair woman
👩‍🦼‍➡️ woman in motorized wheelchair facing right accessibility motorized wheelchair woman
👩‍🦼‍➡️ woman in motorized wheelchair: facing right accessibility facing motorized right wheelchair woman
🧑‍🦽 person in manual wheelchair accessibility manual person wheelchair
🧑‍🦽‍➡️ person in manual wheelchair facing right accessibility manual person wheelchair
🧑‍🦽‍➡️ person in manual wheelchair: facing right accessibility facing manual person right wheelchair
👨‍🦽 man in manual wheelchair accessibility man manual wheelchair
👨‍🦽‍➡️ man in manual wheelchair facing right accessibility man manual wheelchair
👨‍🦽‍➡️ man in manual wheelchair: facing right accessibility facing man manual right wheelchair
👩‍🦽 woman in manual wheelchair accessibility manual wheelchair woman
👩‍🦽‍➡️ woman in manual wheelchair facing right accessibility manual wheelchair woman
👩‍🦽‍➡️ woman in manual wheelchair: facing right accessibility facing manual right wheelchair woman
🏃 person running fast hurry marathon move person quick race racing run rush speed
🏃‍♂️ man running fast hurry man marathon move quick race racing run rush speed
🏃‍♀️ woman running fast hurry marathon move quick race racing run rush speed woman
🏃‍➡️ person running facing right fast hurry marathon move person quick race racing run rush speed
🏃‍♀️‍➡️ woman running facing right fast hurry marathon move quick race racing run rush speed woman
🏃‍♂️‍➡️ man running facing right fast hurry man marathon move quick race racing run rush speed
🏃‍➡️ person running: facing right facing fast hurry marathon move person quick race racing right run rush speed
🏃‍♀️‍➡️ woman running: facing right facing fast hurry marathon move quick race racing right run rush speed woman
🏃‍♂️‍➡️ man running: facing right facing fast hurry man marathon move quick race racing right run rush speed
🧑‍🩰 ballet dancer ballet dancer
💃 woman dancing dance dancer dancing elegant festive flair flamenco groove lets salsa tango woman
🕺 man dancing dance dancer dancing elegant festive flair flamenco groove lets man salsa tango
🕴️ person in suit levitating business levitating person suit
@@ -711,6 +715,7 @@
🐳 spouting whale animal beach face ocean spouting whale
🐋 whale animal beach ocean
🐬 dolphin animal beach flipper ocean
🫍 orca marine ocean whale
🦭 seal animal lion ocean sea
🐟️ fish animal dinner fishes fishing pisces zodiac
🐠 tropical fish animal fish fishes tropical
@@ -910,6 +915,7 @@
🧭 compass direction magnetic navigation orienteering
🏔️ snow-capped mountain cold mountain snow snow-capped
⛰️ mountain mountain
🛘 landslide avalanche danger disaster earthquake mountain mudslide rocks
🌋 volcano eruption mountain nature
🗻 mount fuji fuji mount mountain nature
🏕️ camping camping
@@ -1270,10 +1276,11 @@
🎧️ headphone earbud sound
📻️ radio entertainment tbt video
🎷 saxophone instrument music sax
🎺 trumpet instrument music
🪊 trombone brass instrument jazz music sad slide
🪗 accordion box concertina instrument music squeeze squeezebox
🎸 guitar instrument music strat
🎹 musical keyboard instrument keyboard music musical piano
🎺 trumpet instrument music
🎻 violin instrument music
🪕 banjo music stringed
🥁 drum drumsticks music
@@ -1334,8 +1341,9 @@
📑 bookmark tabs bookmark mark marker tabs
🔖 bookmark mark
🏷️ label tag
💰️ money bag bag bank bet billion cash cost dollar gold million money moneybag paid paying pot rich win
🪙 coin dollar euro gold metal money rich silver treasure
💰️ money bag bag bank bet billion cash cost dollar gold million money moneybag paid paying pot rich win
🪎 treasure chest gem gold jewels loot money prize silver valuables wealth
💴 yen banknote bank banknote bill currency money note yen
💵 dollar banknote bank banknote bill currency dollar money note
💶 euro banknote 100 bank banknote bill currency euro money note rich
@@ -1608,16 +1616,16 @@
🫟 splatter drip holi ink liquid mess paint spill stain
#️⃣ keycap: # keycap
*️⃣ keycap: * keycap
0️⃣ keycap: 0 keycap
1️⃣ keycap: 1 keycap
2️⃣ keycap: 2 keycap
3️⃣ keycap: 3 keycap
4️⃣ keycap: 4 keycap
5️⃣ keycap: 5 keycap
6️⃣ keycap: 6 keycap
7️⃣ keycap: 7 keycap
8️⃣ keycap: 8 keycap
9️⃣ keycap: 9 keycap
0️⃣ keycap: 0 0 keycap zero
1️⃣ keycap: 1 1 keycap one
2️⃣ keycap: 2 2 keycap two
3️⃣ keycap: 3 3 keycap three
4️⃣ keycap: 4 4 four keycap
5️⃣ keycap: 5 5 five keycap
6️⃣ keycap: 6 6 keycap six
7️⃣ keycap: 7 7 keycap seven
8️⃣ keycap: 8 8 eight keycap
9️⃣ keycap: 9 9 keycap nine
🔟 keycap: 10 keycap
🔠 input latin uppercase abcd input latin letters uppercase
🔡 input latin lowercase abcd input latin letters lowercase
@@ -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
@@ -82,6 +82,16 @@ sky cadcff
sapphire aec7ff
blue a6baff
lavender bfcaff
klink 6685d1
klinkSelection 6585d1
kvisited 7276dd
kvisitedSelection 7276dd
knegative 8e70ff
knegativeSelection 8e70ff
kneutral c794ff
kneutralSelection c794ff
kpositive 54afff
kpositiveSelection 54afff
text e4e1e7
subtext1 c6c5d1
subtext0 8f909a
@@ -82,6 +82,16 @@ sky 0082b6
sapphire 037ba6
blue 005e90
lavender 0077b7
klink 2e8fc3
klinkSelection 308fc4
kvisited 2584d6
kvisitedSelection 2984d7
knegative 607eff
knegativeSelection 607eff
kneutral c794ff
kneutralSelection c794ff
kpositive 00b8de
kpositiveSelection 00b8df
text 191c1e
subtext1 41484e
subtext0 6e757c
@@ -82,6 +82,16 @@ sky ccdbff
sapphire b1c6ff
blue aab9ff
lavender c2c9ff
klink 6a84d1
klinkSelection 6a84d1
kvisited 7775dc
kvisitedSelection 7775dc
knegative 946dff
knegativeSelection 946dff
kneutral c794ff
kneutralSelection c794ff
kpositive 5daeff
kpositiveSelection 5eaeff
text e4e1e7
subtext1 c6c5d1
subtext0 90909a
@@ -82,6 +82,16 @@ sky d0daff
sapphire b7c5ff
blue b0b8ff
lavender c7c8ff
klink 7382d2
klinkSelection 7382d2
kvisited 8172da
kvisitedSelection 8172da
knegative a167ff
knegativeSelection a167ff
kneutral ca92ff
kneutralSelection c992ff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7
subtext1 c8c5d1
subtext0 918f9a
@@ -0,0 +1,110 @@
primary_paletteKeyColor 33653E
secondary_paletteKeyColor 1B4E2A
tertiary_paletteKeyColor 376942
neutral_paletteKeyColor 1E1E26
neutral_variant_paletteKeyColor 23252D
background 23262D
onBackground F5F5F6
surface 050505
surfaceDim 1E1E24
surfaceBright 1E1E24
surfaceContainerLowest 0a0a0b
surfaceContainerLow 0a0a0b
surfaceContainer 0a0a0b
surfaceContainerHigh 050505
surfaceContainerHighest 0f1210
onSurface F5F5F6
surfaceVariant 0a0a0b
onSurfaceVariant c9c9c9
inverseSurface 0a0a0b
inverseOnSurface ACACAC
outline 838383
outlineVariant 1E1E25
shadow 000000
scrim 000000
surfaceTint 24BD5C
primary 24BD5C
onPrimary 091f11
primaryContainer 0f1210
onPrimaryContainer 24BD5C
inversePrimary 24BD5C
secondary 24BD5C
onSecondary 043a14
secondaryContainer 0f1210
onSecondaryContainer F4F3F5
tertiary 32653E
onTertiary F5F4F6
tertiaryContainer 1E1E25
onTertiaryContainer F5F5F6
error c66e73
onError F5F4F6
errorContainer 893034
onErrorContainer F5F4F6
primaryFixed 24BD5C
primaryFixedDim 24BD5C
onPrimaryFixed F5F4F6
onPrimaryFixedVariant F5F4F6
secondaryFixed 24BD5C
secondaryFixedDim 24BD5C
onSecondaryFixed F5F4F6
onSecondaryFixedVariant F5F4F6
tertiaryFixed 24BD5C
tertiaryFixedDim 24BD5C
onTertiaryFixed F5F4F6
onTertiaryFixedVariant F5F4F6
term0 343434
term1 23B65A
term2 43ff88
term3 7cfcab
term4 78c19f
term5 7ae9a7
term6 80deb2
term7 ccdcd6
term8 9aa59f
term9 cdff9e
term10 00f608
term11 c9fff3
term12 a4c7cd
term13 a5f7a2
term14 87f1b5
term15 ffffff
rosewater f4f0fa
flamingo dfe0f5
pink bdffd4
mauve 73fa90
red 8affab
maroon abf0c5
peach a9daac
yellow d0f9f4
green 8af797
teal a0f9aa
sky cefb97
sapphire 85ef77
blue 65eea0
lavender 90f79e
klink 65eea0
klinkSelection 65eea0
kvisited 73fa90
kvisitedSelection 73fa90
knegative 8affab
knegativeSelection 8affab
kneutral d0f9f4
kneutralSelection d0f9f4
kpositive 8af797
kpositiveSelection 8af797
text e0e3e4
subtext1 bec8cc
subtext0 889296
overlay2 767f83
overlay1 646c6f
overlay0 535a5d
surface2 43494b
surface1 33383a
surface0 212627
base 101415
mantle 101415
crust 0f1314
success B5CCBA
onSuccess 213528
successContainer 374B3E
onSuccessContainer D1E9D6
@@ -0,0 +1,110 @@
primary_paletteKeyColor 33653E
secondary_paletteKeyColor 1B4E2A
tertiary_paletteKeyColor 376942
neutral_paletteKeyColor 1E1E26
neutral_variant_paletteKeyColor 23252D
background 23262D
onBackground F5F5F6
surface 1E1E24
surfaceDim 1E1E24
surfaceBright 1E1E24
surfaceContainerLowest 23262C
surfaceContainerLow 23262C
surfaceContainer 23262C
surfaceContainerHigh 1b1d22
surfaceContainerHighest 232C29
onSurface F5F5F6
surfaceVariant 23262C
onSurfaceVariant c9c9c9
inverseSurface 23262C
inverseOnSurface ACACAC
outline 979797
outlineVariant 1E1E25
shadow 000000
scrim 000000
surfaceTint 24BD5C
primary 24BD5C
onPrimary 091f11
primaryContainer 232c29
onPrimaryContainer 24BD5C
inversePrimary 24BD5C
secondary 24BD5C
onSecondary 043a14
secondaryContainer 232c29
onSecondaryContainer F4F3F5
tertiary 32653E
onTertiary F5F4F6
tertiaryContainer 1E1E25
onTertiaryContainer F5F5F6
error c66e73
onError F5F4F6
errorContainer 893034
onErrorContainer F5F4F6
primaryFixed 24BD5C
primaryFixedDim 24BD5C
onPrimaryFixed F5F4F6
onPrimaryFixedVariant F5F4F6
secondaryFixed 24BD5C
secondaryFixedDim 24BD5C
onSecondaryFixed F5F4F6
onSecondaryFixedVariant F5F4F6
tertiaryFixed 24BD5C
tertiaryFixedDim 24BD5C
onTertiaryFixed F5F4F6
onTertiaryFixedVariant F5F4F6
term0 343434
term1 23B65A
term2 43ff88
term3 7cfcab
term4 78c19f
term5 7ae9a7
term6 80deb2
term7 ccdcd6
term8 9aa59f
term9 cdff9e
term10 00f608
term11 c9fff3
term12 a4c7cd
term13 a5f7a2
term14 87f1b5
term15 ffffff
rosewater f4f0fa
flamingo dfe0f5
pink bdffd4
mauve 73fa90
red 8affab
maroon abf0c5
peach a9daac
yellow d0f9f4
green 8af797
teal a0f9aa
sky cefb97
sapphire 85ef77
blue 65eea0
lavender 90f79e
klink 65eea0
klinkSelection 65eea0
kvisited 73fa90
kvisitedSelection 73fa90
knegative 8affab
knegativeSelection 8affab
kneutral d0f9f4
kneutralSelection d0f9f4
kpositive 8af797
kpositiveSelection 8af797
text e0e3e4
subtext1 bec8cc
subtext0 889296
overlay2 767f83
overlay1 646c6f
overlay0 535a5d
surface2 43494b
surface1 33383a
surface0 212627
base 101415
mantle 101415
crust 0f1314
success B5CCBA
onSuccess 213528
successContainer 374B3E
onSuccessContainer D1E9D6
@@ -0,0 +1,110 @@
primary_paletteKeyColor BD93F9
secondary_paletteKeyColor 50FA7B
tertiary_paletteKeyColor FF79C6
neutral_paletteKeyColor 282A36
neutral_variant_paletteKeyColor 44475A
background 282A36
onBackground F8F8F2
surface 343746
surfaceDim 21222C
surfaceBright 4D4F66
surfaceContainerLowest 191A21
surfaceContainerLow 3C3F4E
surfaceContainer 3E4153
surfaceContainerHigh 4D4F66
surfaceContainerHighest 565970
onSurface F8F8F2
surfaceVariant 3E4153
onSurfaceVariant F8F8F2
inverseSurface F8F8F2
inverseOnSurface 282A36
outline 6272A4
outlineVariant 4D4F66
shadow 000000
scrim 000000
surfaceTint BD93F9
primary BD93F9
onPrimary 282A36
primaryContainer 4D4F66
onPrimaryContainer BD93F9
inversePrimary 9D73D9
secondary 50FA7B
onSecondary 282A36
secondaryContainer 4D4F66
onSecondaryContainer 50FA7B
tertiary FF79C6
onTertiary 282A36
tertiaryContainer 4D4F66
onTertiaryContainer FF79C6
error FF5555
onError 282A36
errorContainer 4C3743
onErrorContainer FF5555
primaryFixed BD93F9
primaryFixedDim 9D73D9
onPrimaryFixed 282A36
onPrimaryFixedVariant 3E4153
secondaryFixed 50FA7B
secondaryFixedDim 30DA5B
onSecondaryFixed 282A36
onSecondaryFixedVariant 3E4153
tertiaryFixed FF79C6
tertiaryFixedDim DF59A6
onTertiaryFixed 282A36
onTertiaryFixedVariant 3E4153
term0 282A36
term1 FF5555
term2 50FA7B
term3 F1FA8C
term4 BD93F9
term5 FF79C6
term6 8BE9FD
term7 F8F8F2
term8 6272A4
term9 FF6E6E
term10 69FF94
term11 FFFFA5
term12 D6ACFF
term13 FF92DF
term14 A4FFFF
term15 FFFFFF
rosewater F8F8F2
flamingo FFB86C
pink FF79C6
mauve BD93F9
red FF5555
maroon FF6E6E
peach FFB86C
yellow F1FA8C
green 50FA7B
teal 8BE9FD
sky 8BE9FD
sapphire 8BE9FD
blue BD93F9
lavender BD93F9
klink BD93F9
klinkSelection BD93F9
kvisited FF79C6
kvisitedSelection FF79C6
knegative FF5555
knegativeSelection FF5555
kneutral F1FA8C
kneutralSelection F1FA8C
kpositive 50FA7B
kpositiveSelection 50FA7B
text F8F8F2
subtext1 F8F8F2
subtext0 E6E6E6
overlay2 A0A0A0
overlay1 8A8A8A
overlay0 6272A4
surface2 3E4153
surface1 343746
surface0 282A36
base 282A36
mantle 21222C
crust 191A21
success 50FA7B
onSuccess 282A36
successContainer 4D4F66
onSuccessContainer F8F8F2
@@ -0,0 +1,110 @@
primary_paletteKeyColor 8CCFB0
secondary_paletteKeyColor E5C76B
tertiary_paletteKeyColor E5A5C5
neutral_paletteKeyColor 2D3139
neutral_variant_paletteKeyColor 3A3F4B
background 141B1E
onBackground E8E8E8
surface 232A2D
surfaceDim 0F1416
surfaceBright 3A4145
surfaceContainerLowest 0A0E10
surfaceContainerLow 2A3235
surfaceContainer 2E3538
surfaceContainerHigh 3A4145
surfaceContainerHighest 434A4E
onSurface E8E8E8
surfaceVariant 2E3538
onSurfaceVariant B3B9BE
inverseSurface E8E8E8
inverseOnSurface 141B1E
outline 8A8F94
outlineVariant 3A4145
shadow 000000
scrim 000000
surfaceTint 8CCFB0
primary 8CCFB0
onPrimary 141B1E
primaryContainer 3A4145
onPrimaryContainer 8CCFB0
inversePrimary 6FA98C
secondary E5C76B
onSecondary 141B1E
secondaryContainer 3A4145
onSecondaryContainer E5C76B
tertiary E5A5C5
onTertiary 141B1E
tertiaryContainer 3A4145
onTertiaryContainer E5A5C5
error E57474
onError 141B1E
errorContainer 4A2C2C
onErrorContainer E57474
primaryFixed 8CCFB0
primaryFixedDim 6FA98C
onPrimaryFixed 141B1E
onPrimaryFixedVariant 3A3F4B
secondaryFixed E5C76B
secondaryFixedDim C4A855
onSecondaryFixed 141B1E
onSecondaryFixedVariant 3A3F4B
tertiaryFixed E5A5C5
tertiaryFixedDim C888A4
onTertiaryFixed 141B1E
onTertiaryFixedVariant 3A3F4B
term0 141B1E
term1 E57474
term2 8CCFB0
term3 E5C76B
term4 67B0E8
term5 C47FD5
term6 6CBFBF
term7 E8E8E8
term8 8A8F94
term9 E57474
term10 8CCFB0
term11 E5C76B
term12 67B0E8
term13 C47FD5
term14 6CBFBF
term15 E8E8E8
rosewater E8E8E8
flamingo E5A5C5
pink E5A5C5
mauve C47FD5
red E57474
maroon E57474
peach E59A84
yellow E5C76B
green 8CCFB0
teal 6CBFBF
sky 67B0E8
sapphire 67B0E8
blue 67B0E8
lavender 67B0E8
klink 67B0E8
klinkSelection 67B0E8
kvisited C47FD5
kvisitedSelection C47FD5
knegative E57474
knegativeSelection E57474
kneutral E5C76B
kneutralSelection E5C76B
kpositive 8CCFB0
kpositiveSelection 8CCFB0
text E8E8E8
subtext1 B3B9BE
subtext0 8A8F94
overlay2 7A7F84
overlay1 6A6F74
overlay0 5A5F64
surface2 2E3538
surface1 232A2D
surface0 1A2023
base 141B1E
mantle 0F1416
crust 0A0E10
success 8CCFB0
onSuccess 141B1E
successContainer 3A4145
onSuccessContainer E8E8E8
@@ -0,0 +1,110 @@
primary_paletteKeyColor 7FBBB3
secondary_paletteKeyColor 83C092
tertiary_paletteKeyColor A7C080
neutral_paletteKeyColor 2E383C
neutral_variant_paletteKeyColor 374145
background 1E2326
onBackground D3C6AA
surface 252B2E
surfaceDim 15191C
surfaceBright 343E43
surfaceContainerLowest 11161A
surfaceContainerLow 2A3338
surfaceContainer 2E383C
surfaceContainerHigh 343E43
surfaceContainerHighest 3A4448
onSurface D3C6AA
surfaceVariant 374145
onSurfaceVariant 9DA9A0
inverseSurface D3C6AA
inverseOnSurface 1E2326
outline 859289
outlineVariant 414B50
shadow 000000
scrim 000000
surfaceTint 7FBBB3
primary 7FBBB3
onPrimary 1E2326
primaryContainer 414B50
onPrimaryContainer A7C080
inversePrimary 5A9A8F
secondary 83C092
onSecondary 1E2326
secondaryContainer 414B50
onSecondaryContainer A7C080
tertiary A7C080
onTertiary 1E2326
tertiaryContainer 414B50
onTertiaryContainer D3C6AA
error E67E80
onError 1E2326
errorContainer 4C3743
onErrorContainer E67E80
primaryFixed 7FBBB3
primaryFixedDim 5A9A8F
onPrimaryFixed 1E2326
onPrimaryFixedVariant 374145
secondaryFixed 83C092
secondaryFixedDim 5F8C6F
onSecondaryFixed 1E2326
onSecondaryFixedVariant 374145
tertiaryFixed A7C080
tertiaryFixedDim 7F9D5F
onTertiaryFixed 1E2326
onTertiaryFixedVariant 374145
term0 1E2326
term1 E67E80
term2 A7C080
term3 DBBC7F
term4 7FBBB3
term5 D699B6
term6 83C092
term7 D3C6AA
term8 859289
term9 E67E80
term10 A7C080
term11 DBBC7F
term12 7FBBB3
term13 D699B6
term14 83C092
term15 D3C6AA
rosewater D3C6AA
flamingo D699B6
pink D699B6
mauve D699B6
red E67E80
maroon E67E80
peach E69875
yellow DBBC7F
green A7C080
teal 83C092
sky 7FBBB3
sapphire 7FBBB3
blue 7FBBB3
lavender 7FBBB3
klink 7FBBB3
klinkSelection 7FBBB3
kvisited 83C092
kvisitedSelection 83C092
knegative E67E80
knegativeSelection E67E80
kneutral DBBC7F
kneutralSelection DBBC7F
kpositive A7C080
kpositiveSelection A7C080
text D3C6AA
subtext1 9DA9A0
subtext0 859289
overlay2 7A8478
overlay1 6F7A6F
overlay0 5F6A5F
surface2 2E383C
surface1 252B2E
surface0 1E2326
base 1E2326
mantle 15191C
crust 11161A
success A7C080
onSuccess 1E2326
successContainer 414B50
onSuccessContainer D3C6AA
@@ -0,0 +1,110 @@
primary_paletteKeyColor 7FBBB3
secondary_paletteKeyColor 83C092
tertiary_paletteKeyColor A7C080
neutral_paletteKeyColor 2E383C
neutral_variant_paletteKeyColor 374145
background 2D353B
onBackground D3C6AA
surface 343F44
surfaceDim 232A2E
surfaceBright 475258
surfaceContainerLowest 1E2326
surfaceContainerLow 3B474E
surfaceContainer 3D484D
surfaceContainerHigh 475258
surfaceContainerHighest 4C5258
onSurface D3C6AA
surfaceVariant 3D484D
onSurfaceVariant 9DA9A0
inverseSurface D3C6AA
inverseOnSurface 2D353B
outline 859289
outlineVariant 475258
shadow 000000
scrim 000000
surfaceTint 7FBBB3
primary 7FBBB3
onPrimary 2D353B
primaryContainer 475258
onPrimaryContainer A7C080
inversePrimary 5A9A8F
secondary 83C092
onSecondary 2D353B
secondaryContainer 475258
onSecondaryContainer A7C080
tertiary A7C080
onTertiary 2D353B
tertiaryContainer 475258
onTertiaryContainer D3C6AA
error E67E80
onError 2D353B
errorContainer 4C3743
onErrorContainer E67E80
primaryFixed 7FBBB3
primaryFixedDim 5A9A8F
onPrimaryFixed 2D353B
onPrimaryFixedVariant 374145
secondaryFixed 83C092
secondaryFixedDim 5F8C6F
onSecondaryFixed 2D353B
onSecondaryFixedVariant 374145
tertiaryFixed A7C080
tertiaryFixedDim 7F9D5F
onTertiaryFixed 2D353B
onTertiaryFixedVariant 374145
term0 2D353B
term1 E67E80
term2 A7C080
term3 DBBC7F
term4 7FBBB3
term5 D699B6
term6 83C092
term7 D3C6AA
term8 859289
term9 E67E80
term10 A7C080
term11 DBBC7F
term12 7FBBB3
term13 D699B6
term14 83C092
term15 D3C6AA
rosewater D3C6AA
flamingo D699B6
pink D699B6
mauve D699B6
red E67E80
maroon E67E80
peach E69875
yellow DBBC7F
green A7C080
teal 83C092
sky 7FBBB3
sapphire 7FBBB3
blue 7FBBB3
lavender 7FBBB3
klink 7FBBB3
klinkSelection 7FBBB3
kvisited 83C092
kvisitedSelection 83C092
knegative E67E80
knegativeSelection E67E80
kneutral DBBC7F
kneutralSelection DBBC7F
kpositive A7C080
kpositiveSelection A7C080
text D3C6AA
subtext1 9DA9A0
subtext0 859289
overlay2 7A8478
overlay1 6F7A6F
overlay0 5F6A5F
surface2 3D484D
surface1 343F44
surface0 2D353B
base 2D353B
mantle 232A2E
crust 1E2326
success A7C080
onSuccess 2D353B
successContainer 475258
onSuccessContainer D3C6AA
@@ -0,0 +1,110 @@
primary_paletteKeyColor 3A94C5
secondary_paletteKeyColor 35A77C
tertiary_paletteKeyColor 8DA101
neutral_paletteKeyColor E6E2CC
neutral_variant_paletteKeyColor E0DCC7
background FDF6E3
onBackground 5C6A72
surface F3EAD3
surfaceDim FDF6E3
surfaceBright FFFBF0
surfaceContainerLowest FFFBF0
surfaceContainerLow FDF6E3
surfaceContainer F3EAD3
surfaceContainerHigh EAE4CA
surfaceContainerHighest E0DCC7
onSurface 5C6A72
surfaceVariant EAE4CA
onSurfaceVariant 6F7C84
inverseSurface 5C6A72
inverseOnSurface FDF6E3
outline 939F91
outlineVariant E0DCC7
shadow 000000
scrim 000000
surfaceTint 3A94C5
primary 3A94C5
onPrimary FFFBF0
primaryContainer E0DCC7
onPrimaryContainer 8DA101
inversePrimary 5FAFD7
secondary 35A77C
onSecondary FFFBF0
secondaryContainer E0DCC7
onSecondaryContainer 8DA101
tertiary 8DA101
onTertiary FFFBF0
tertiaryContainer E0DCC7
onTertiaryContainer 5C6A72
error F85552
onError FFFBF0
errorContainer E6E2CC
onErrorContainer F85552
primaryFixed 3A94C5
primaryFixedDim 5FAFD7
onPrimaryFixed FFFBF0
onPrimaryFixedVariant E0DCC7
secondaryFixed 35A77C
secondaryFixedDim 5FC198
onSecondaryFixed FFFBF0
onSecondaryFixedVariant E0DCC7
tertiaryFixed 8DA101
tertiaryFixedDim A7C080
onTertiaryFixed FFFBF0
onTertiaryFixedVariant E0DCC7
term0 5C6A72
term1 F85552
term2 8DA101
term3 DFA000
term4 3A94C5
term5 DF69BA
term6 35A77C
term7 5C6A72
term8 939F91
term9 F85552
term10 8DA101
term11 DFA000
term12 3A94C5
term13 DF69BA
term14 35A77C
term15 5C6A72
rosewater 5C6A72
flamingo DF69BA
pink DF69BA
mauve DF69BA
red F85552
maroon F85552
peach E66868
yellow DFA000
green 8DA101
teal 35A77C
sky 3A94C5
sapphire 3A94C5
blue 3A94C5
lavender 3A94C5
klink 3A94C5
klinkSelection 3A94C5
kvisited 35A77C
kvisitedSelection 35A77C
knegative F85552
knegativeSelection F85552
kneutral DFA000
kneutralSelection DFA000
kpositive 8DA101
kpositiveSelection 8DA101
text 5C6A72
subtext1 6F7C84
subtext0 939F91
overlay2 A6B0A0
overlay1 B9C0B0
overlay0 CCD3C2
surface2 EAE4CA
surface1 F3EAD3
surface0 FDF6E3
base FDF6E3
mantle FFFBF0
crust FFFEF9
success 8DA101
onSuccess FFFBF0
successContainer E0DCC7
onSuccessContainer 5C6A72
@@ -0,0 +1,110 @@
primary_paletteKeyColor 7FBBB3
secondary_paletteKeyColor 83C092
tertiary_paletteKeyColor A7C080
neutral_paletteKeyColor 2E383C
neutral_variant_paletteKeyColor 374145
background 323C41
onBackground D3C6AA
surface 3A454A
surfaceDim 282F34
surfaceBright 4D585D
surfaceContainerLowest 232A2E
surfaceContainerLow 414D54
surfaceContainer 434E53
surfaceContainerHigh 4D585D
surfaceContainerHighest 525C61
onSurface D3C6AA
surfaceVariant 434E53
onSurfaceVariant 9DA9A0
inverseSurface D3C6AA
inverseOnSurface 323C41
outline 859289
outlineVariant 4D585D
shadow 000000
scrim 000000
surfaceTint 7FBBB3
primary 7FBBB3
onPrimary 323C41
primaryContainer 4D585D
onPrimaryContainer A7C080
inversePrimary 5A9A8F
secondary 83C092
onSecondary 323C41
secondaryContainer 4D585D
onSecondaryContainer A7C080
tertiary A7C080
onTertiary 323C41
tertiaryContainer 4D585D
onTertiaryContainer D3C6AA
error E67E80
onError 323C41
errorContainer 4C3743
onErrorContainer E67E80
primaryFixed 7FBBB3
primaryFixedDim 5A9A8F
onPrimaryFixed 323C41
onPrimaryFixedVariant 374145
secondaryFixed 83C092
secondaryFixedDim 5F8C6F
onSecondaryFixed 323C41
onSecondaryFixedVariant 374145
tertiaryFixed A7C080
tertiaryFixedDim 7F9D5F
onTertiaryFixed 323C41
onTertiaryFixedVariant 374145
term0 323C41
term1 E67E80
term2 A7C080
term3 DBBC7F
term4 7FBBB3
term5 D699B6
term6 83C092
term7 D3C6AA
term8 859289
term9 E67E80
term10 A7C080
term11 DBBC7F
term12 7FBBB3
term13 D699B6
term14 83C092
term15 D3C6AA
rosewater D3C6AA
flamingo D699B6
pink D699B6
mauve D699B6
red E67E80
maroon E67E80
peach E69875
yellow DBBC7F
green A7C080
teal 83C092
sky 7FBBB3
sapphire 7FBBB3
blue 7FBBB3
lavender 7FBBB3
klink 7FBBB3
klinkSelection 7FBBB3
kvisited 83C092
kvisitedSelection 83C092
knegative E67E80
knegativeSelection E67E80
kneutral DBBC7F
kneutralSelection DBBC7F
kpositive A7C080
kpositiveSelection A7C080
text D3C6AA
subtext1 9DA9A0
subtext0 859289
overlay2 7A8478
overlay1 6F7A6F
overlay0 5F6A5F
surface2 434E53
surface1 3A454A
surface0 323C41
base 323C41
mantle 282F34
crust 232A2E
success A7C080
onSuccess 323C41
successContainer 4D585D
onSuccessContainer D3C6AA
@@ -82,6 +82,16 @@ sky 97e7fb
sapphire 77d4ef
blue 65c9ee
lavender 90d6f7
klink 0093b4
klinkSelection 0093b3
kvisited 0089bf
kvisitedSelection 0089be
knegative 607eff
knegativeSelection 607eff
kneutral 34c359
kneutralSelection 34c359
kpositive 00bbc7
kpositiveSelection 00bbc7
text e0e3e4
subtext1 bec8cc
subtext0 889296
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 5d7c2e
blue 00664e
lavender 00816c
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative a78300
knegativeSelection a78300
kneutral c7a900
kneutralSelection c7a900
kpositive a0b31d
kpositiveSelection a1b31c
text 1c1c16
subtext1 494739
subtext0 777565
@@ -82,6 +82,16 @@ sky 94e8f6
sapphire 74d5e9
blue 5fcae8
lavender 8cd7f3
klink 0094ac
klinkSelection 0094ab
kvisited 008bb6
kvisitedSelection 008bb5
knegative 607eff
knegativeSelection 607eff
kneutral 34c359
kneutralSelection 34c359
kpositive 00bcbf
kpositiveSelection 00bcbd
text e0e3e4
subtext1 bec8ca
subtext0 889394
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 657b26
blue 00664e
lavender 00816c
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative ae8000
knegativeSelection ae8000
kneutral d1a500
kneutralSelection d0a500
kpositive adaf00
kpositiveSelection adaf00
text 1d1c15
subtext1 4a4738
subtext0 797564
@@ -82,6 +82,16 @@ sky e1df87
sapphire b3d27e
blue ffa2bd
lavender ffbcbb
klink bf6ba0
klinkSelection bf6ba0
kvisited cc6232
kvisitedSelection cc6232
knegative d66a00
knegativeSelection d66900
kneutral ff8d00
kneutralSelection ff8d06
kpositive de9d00
kpositiveSelection df9d00
text ece0d9
subtext1 d6c3b5
subtext0 9f8e81
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 6a7a22
blue 00664e
lavender c2484e
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative b27f00
knegativeSelection b27f00
kneutral d5a300
kneutralSelection d5a300
kpositive b3ae00
kpositiveSelection b3ae00
text 1d1b15
subtext1 4b4738
subtext0 7a7464
@@ -0,0 +1,110 @@
primary_paletteKeyColor 88C0D0
secondary_paletteKeyColor 81A1C1
tertiary_paletteKeyColor 5E81AC
neutral_paletteKeyColor 3B4252
neutral_variant_paletteKeyColor 434C5E
background 2E3440
onBackground ECEFF4
surface 3B4252
surfaceDim 242933
surfaceBright 4C566A
surfaceContainerLowest 1F232C
surfaceContainerLow 424A5E
surfaceContainer 434C5E
surfaceContainerHigh 4C566A
surfaceContainerHighest 55606E
onSurface ECEFF4
surfaceVariant 434C5E
onSurfaceVariant D8DEE9
inverseSurface ECEFF4
inverseOnSurface 2E3440
outline 616E88
outlineVariant 4C566A
shadow 000000
scrim 000000
surfaceTint 88C0D0
primary 88C0D0
onPrimary 2E3440
primaryContainer 4C566A
onPrimaryContainer 88C0D0
inversePrimary 6FA3B3
secondary 81A1C1
onSecondary 2E3440
secondaryContainer 4C566A
onSecondaryContainer 81A1C1
tertiary 5E81AC
onTertiary 2E3440
tertiaryContainer 4C566A
onTertiaryContainer 5E81AC
error BF616A
onError 2E3440
errorContainer 4C3743
onErrorContainer BF616A
primaryFixed 88C0D0
primaryFixedDim 6FA3B3
onPrimaryFixed 2E3440
onPrimaryFixedVariant 434C5E
secondaryFixed 81A1C1
secondaryFixedDim 6A84A4
onSecondaryFixed 2E3440
onSecondaryFixedVariant 434C5E
tertiaryFixed 5E81AC
tertiaryFixedDim 4A6A8F
onTertiaryFixed 2E3440
onTertiaryFixedVariant 434C5E
term0 3B4252
term1 BF616A
term2 A3BE8C
term3 EBCB8B
term4 81A1C1
term5 B48EAD
term6 88C0D0
term7 E5E9F0
term8 4C566A
term9 BF616A
term10 A3BE8C
term11 EBCB8B
term12 81A1C1
term13 B48EAD
term14 8FBCBB
term15 ECEFF4
rosewater ECEFF4
flamingo B48EAD
pink B48EAD
mauve B48EAD
red BF616A
maroon BF616A
peach D08770
yellow EBCB8B
green A3BE8C
teal 8FBCBB
sky 88C0D0
sapphire 81A1C1
blue 5E81AC
lavender 5E81AC
klink 88C0D0
klinkSelection 88C0D0
kvisited 81A1C1
kvisitedSelection 81A1C1
knegative BF616A
knegativeSelection BF616A
kneutral EBCB8B
kneutralSelection EBCB8B
kpositive A3BE8C
kpositiveSelection A3BE8C
text ECEFF4
subtext1 D8DEE9
subtext0 616E88
overlay2 5A677E
overlay1 4F5B73
overlay0 434C5E
surface2 434C5E
surface1 3B4252
surface0 2E3440
base 2E3440
mantle 242933
crust 1F232C
success A3BE8C
onSuccess 2E3440
successContainer 4C566A
onSuccessContainer ECEFF4
@@ -82,6 +82,16 @@ sky c4ddff
sapphire a4caff
blue 9abdff
lavender b7ccff
klink 5689ce
klinkSelection 5689ce
kvisited 5f7bdd
kvisitedSelection 5f7bdd
knegative 7877ff
knegativeSelection 7878ff
kneutral c794ff
kneutralSelection c794ff
kpositive 13b3ff
kpositiveSelection 0db3ff
text e3e2e7
subtext1 c4c6d0
subtext0 8e909a
@@ -82,6 +82,16 @@ sky c2deff
sapphire a1caff
blue 97beff
lavender b5cdff
klink 5389ce
klinkSelection 5489ce
kvisited 5b7cdd
kvisitedSelection 5c7bdd
knegative 7479ff
knegativeSelection 7578ff
kneutral c794ff
kneutralSelection c794ff
kpositive 00b4fd
kpositiveSelection 00b4fe
text e3e2e7
subtext1 c3c6d0
subtext0 8d919a
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 6d791e
blue 00664e
lavender c2484e
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative b47d00
knegativeSelection b57d00
kneutral d8a200
kneutralSelection d9a200
kpositive b7ac00
kpositiveSelection b8ac00
text 1e1b15
subtext1 4c4638
subtext0 7b7464
@@ -82,6 +82,16 @@ sky d3d9ff
sapphire bdc3ff
blue b7b6ff
lavender ccc6ff
klink 7b80d1
klinkSelection 7b80d1
kvisited 8a6fd7
kvisitedSelection 8a6fd7
knegative ac62fa
knegativeSelection ac62fa
kneutral d48dff
kneutralSelection d48eff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7
subtext1 c9c4d0
subtext0 938f9a
@@ -82,6 +82,16 @@ sky d2d9ff
sapphire bbc4ff
blue b5b6ff
lavender cbc7ff
klink 7880d1
klinkSelection 7881d1
kvisited 8770d8
kvisitedSelection 8770d8
knegative a964fd
knegativeSelection a864fd
kneutral d08fff
kneutralSelection d090ff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7
subtext1 c9c5d0
subtext0 928f9a
@@ -82,6 +82,16 @@ sky cedaff
sapphire b5c5ff
blue aeb8ff
lavender c6c8ff
klink 7083d2
klinkSelection 6f83d2
kvisited 7e73db
kvisitedSelection 7d73db
knegative 9d69ff
knegativeSelection 9b6aff
kneutral c794ff
kneutralSelection c794ff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7
subtext1 c7c5d1
subtext0 918f9a
@@ -0,0 +1,110 @@
primary_paletteKeyColor 268BD2
secondary_paletteKeyColor 2AA198
tertiary_paletteKeyColor 6C71C4
neutral_paletteKeyColor 002B36
neutral_variant_paletteKeyColor 073642
background 002B36
onBackground FDF6E3
surface 073642
surfaceDim 001F29
surfaceBright 0D4250
surfaceContainerLowest 00151D
surfaceContainerLow 0A404E
surfaceContainer 094B59
surfaceContainerHigh 0D4250
surfaceContainerHighest 11505E
onSurface FDF6E3
surfaceVariant 094B59
onSurfaceVariant 93A1A1
inverseSurface FDF6E3
inverseOnSurface 002B36
outline 586E75
outlineVariant 0D4250
shadow 000000
scrim 000000
surfaceTint 268BD2
primary 268BD2
onPrimary 002B36
primaryContainer 0D4250
onPrimaryContainer 268BD2
inversePrimary 2075B2
secondary 2AA198
onSecondary 002B36
secondaryContainer 0D4250
onSecondaryContainer 2AA198
tertiary 6C71C4
onTertiary 002B36
tertiaryContainer 0D4250
onTertiaryContainer 6C71C4
error DC322F
onError 002B36
errorContainer 4C3743
onErrorContainer DC322F
primaryFixed 268BD2
primaryFixedDim 2075B2
onPrimaryFixed 002B36
onPrimaryFixedVariant 094B59
secondaryFixed 2AA198
secondaryFixedDim 228178
onSecondaryFixed 002B36
onSecondaryFixedVariant 094B59
tertiaryFixed 6C71C4
tertiaryFixedDim 5C61A4
onTertiaryFixed 002B36
onTertiaryFixedVariant 094B59
term0 002B36
term1 DC322F
term2 859900
term3 B58900
term4 268BD2
term5 D33682
term6 2AA198
term7 EEE8D5
term8 586E75
term9 CB4B16
term10 859900
term11 B58900
term12 268BD2
term13 6C71C4
term14 2AA198
term15 FDF6E3
rosewater FDF6E3
flamingo EEE8D5
pink D33682
mauve 6C71C4
red DC322F
maroon CB4B16
peach CB4B16
yellow B58900
green 859900
teal 2AA198
sky 2AA198
sapphire 268BD2
blue 268BD2
lavender 6C71C4
klink 268BD2
klinkSelection 268BD2
kvisited 6C71C4
kvisitedSelection 6C71C4
knegative DC322F
knegativeSelection DC322F
kneutral B58900
kneutralSelection B58900
kpositive 859900
kpositiveSelection 859900
text FDF6E3
subtext1 93A1A1
subtext0 839496
overlay2 657B83
overlay1 586E75
overlay0 073642
surface2 094B59
surface1 073642
surface0 002B36
base 002B36
mantle 001F29
crust 00151D
success 859900
onSuccess 002B36
successContainer 0D4250
onSuccessContainer FDF6E3
@@ -0,0 +1,110 @@
primary_paletteKeyColor 7AA2F7
secondary_paletteKeyColor 9ECE6A
tertiary_paletteKeyColor BB9AF7
neutral_paletteKeyColor 1A1B26
neutral_variant_paletteKeyColor 292E42
background 1A1B26
onBackground C0CAF5
surface 24283B
surfaceDim 16161E
surfaceBright 3B4261
surfaceContainerLowest 0F0F14
surfaceContainerLow 2B3048
surfaceContainer 2A2F41
surfaceContainerHigh 3B4261
surfaceContainerHighest 414868
onSurface C0CAF5
surfaceVariant 2A2F41
onSurfaceVariant A9B1D6
inverseSurface C0CAF5
inverseOnSurface 1A1B26
outline 565F89
outlineVariant 3B4261
shadow 000000
scrim 000000
surfaceTint 7AA2F7
primary 7AA2F7
onPrimary 1A1B26
primaryContainer 3B4261
onPrimaryContainer 7AA2F7
inversePrimary 5A7FD7
secondary 9ECE6A
onSecondary 1A1B26
secondaryContainer 3B4261
onSecondaryContainer 9ECE6A
tertiary BB9AF7
onTertiary 1A1B26
tertiaryContainer 3B4261
onTertiaryContainer BB9AF7
error F7768E
onError 1A1B26
errorContainer 4C3743
onErrorContainer F7768E
primaryFixed 7AA2F7
primaryFixedDim 5A7FD7
onPrimaryFixed 1A1B26
onPrimaryFixedVariant 2A2F41
secondaryFixed 9ECE6A
secondaryFixedDim 7EAE4A
onSecondaryFixed 1A1B26
onSecondaryFixedVariant 2A2F41
tertiaryFixed BB9AF7
tertiaryFixedDim 9B7AD7
onTertiaryFixed 1A1B26
onTertiaryFixedVariant 2A2F41
term0 1A1B26
term1 F7768E
term2 9ECE6A
term3 E0AF68
term4 7AA2F7
term5 BB9AF7
term6 7DCFFF
term7 C0CAF5
term8 565F89
term9 F7768E
term10 9ECE6A
term11 E0AF68
term12 7AA2F7
term13 BB9AF7
term14 7DCFFF
term15 C0CAF5
rosewater C0CAF5
flamingo BB9AF7
pink F7768E
mauve BB9AF7
red F7768E
maroon E0AF68
peach FF9E64
yellow E0AF68
green 9ECE6A
teal 1ABC9C
sky 7DCFFF
sapphire 2AC3DE
blue 7AA2F7
lavender 7DCFFF
klink 7AA2F7
klinkSelection 7AA2F7
kvisited BB9AF7
kvisitedSelection BB9AF7
knegative F7768E
knegativeSelection F7768E
kneutral E0AF68
kneutralSelection E0AF68
kpositive 9ECE6A
kpositiveSelection 9ECE6A
text C0CAF5
subtext1 A9B1D6
subtext0 9AA5CE
overlay2 787C99
overlay1 696D85
overlay0 565F89
surface2 2A2F41
surface1 24283B
surface0 1A1B26
base 1A1B26
mantle 16161E
crust 0F0F14
success 9ECE6A
onSuccess 1A1B26
successContainer 3B4261
onSuccessContainer C0CAF5
+55
View File
@@ -0,0 +1,55 @@
# Cava Audio Visualizer Configuration Template
# Optimized for smooth and responsive visualization
[general]
# Framerate (1-144) - higher = smoother but more CPU intensive
framerate = 60
[input]
# Audio input method: pulse, alsa, fifo, or portaudio
method = pulse
# Audio device (leave as default for auto-detection)
source = auto
[output]
# Output method: ncurses, terminal, raw, or circle
method = ncurses
# Terminal color scheme
style = stereo
[color]
# Color gradient for bars using template variables
gradient = 1
gradient_count = 8
gradient_color_1 = '{{ $green }}'
gradient_color_2 = '{{ $teal }}'
gradient_color_3 = '{{ $sky }}'
gradient_color_4 = '{{ $sapphire }}'
gradient_color_5 = '{{ $blue }}'
gradient_color_6 = '{{ $lavender }}'
gradient_color_7 = '{{ $mauve }}'
gradient_color_8 = '{{ $maroon }}'
[smoothing]
# Noise reduction (0-100) - higher = smoother but less responsive
# 77 is default, 85 provides good balance for smooth visualization
noise_reduction = 85
# Monstercat smoothing (0 or 1) - adds smoothing between adjacent bars
monstercat = 1
# Wave effect (0 or 1) - creates wave-like motion across bars
waves = 0
# Gravity (0-200) - controls how fast bars fall
# 100 = normal gravity, 150 = faster fall, 50 = slower fall
gravity = 120
[eq]
# Equalizer settings for frequency response
# Lower frequencies tend to be louder, so reduce them slightly
1 = 0.8
2 = 0.9
3 = 1.0
4 = 1.1
5 = 1.2
+12 -10
View File
@@ -15,8 +15,10 @@
@import url("https://refact0r.github.io/midnight-discord/build/midnight.css");
body {
/* font, change to '' for default discord font */
--font: "figtree";
/* font options */
--font: "figtree"; /* change to '' for default discord font */
--code-font: "JetBrainsMono NF"; /* change to '' for default discord font */
font-weight: 400; /* normal text font weight. DOES NOT AFFECT BOLD TEXT */
/* sizes */
--gap: 12px; /* spacing between panels */
@@ -27,13 +29,14 @@ body {
--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 */
--border-hover-transition: 0.2s ease; /* transition for borders when hovered */
/* 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) */
--top-bar-button-position: titlebar; /* 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: off; /* 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 */
@@ -42,9 +45,9 @@ body {
/* 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-url: url("https://refact0r.github.io/midnight-discord/assets/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-before: var(--icon-subtle); /* normal icon color */
--dms-icon-color-after: var(--white); /* icon color when button is hovered/selected */
/* dms button background options */
@@ -71,12 +74,11 @@ body {
--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. */
--custom-chatbar: off; /* off: default chatbar, separated: chatbar separated from chat */
--chatbar-height: 47px; /* height of the chatbar (56px by default, 47px to align with user panel, 56px recommended for separated) */
/* other options */
--small-user-panel: off; /* turn on to make the user panel smaller like in old discord */
--small-user-panel: off; /* off: default user panel, on: smaller user panel like in old discord */
}
/* color options */
+4
View File
@@ -15,3 +15,7 @@
@define-color sidebar_fg_color @window_fg_color;
@define-color sidebar_border_color @window_bg_color;
@define-color sidebar_backdrop_color @window_bg_color;
@define-color theme_selected_bg_color alpha(@accent_color, 0.15);
@define-color theme_selected_fg_color @primary;
@import "thunar.css";
+95
View File
@@ -0,0 +1,95 @@
# HTOP Color Configuration Template
# This template generates a custom htoprc configuration with themed colors
# Colors are defined using terminal color codes (0-255) or RGB hex values
# htoprc configuration with custom colors
fields=0 48 17 18 38 39 40 2 46 47 49 1
sort_key=46
sort_direction=-1
tree_sort_key=0
tree_sort_direction=1
hide_kernel_threads=1
hide_userland_threads=0
shadow_other_users=0
show_thread_names=0
show_program_path=1
highlight_base_name=0
highlight_deleted_exe=1
highlight_megabytes=1
highlight_threads=1
highlight_changes=0
highlight_changes_delay_secs=5
find_comm_in_cmdline=1
strip_exe_from_cmdline=1
show_merged_command=0
tree_view=0
tree_view_always_by_pid=0
all_branches_collapsed=0
header_margin=1
detailed_cpu_time=0
cpu_count_from_one=0
show_cpu_usage=1
show_cpu_frequency=0
show_cpu_temperature=0
degree_fahrenheit=0
update_process_names=0
account_guest_in_cpu_meter=0
color_scheme=6
# Custom color definitions using template variables
# Main interface colors
color_background={{ $surface }}
color_text={{ $onSurface }}
color_highlight={{ $primary }}
color_selected={{ $surfaceContainer }}
# CPU meter colors (gradient)
color_cpu_low={{ $green }}
color_cpu_med={{ $yellow }}
color_cpu_high={{ $red }}
# Memory meter colors
color_mem_used={{ $blue }}
color_mem_buffers={{ $teal }}
color_mem_cache={{ $sapphire }}
color_mem_available={{ $green }}
# Process list colors
color_process_normal={{ $onSurface }}
color_process_running={{ $green }}
color_process_sleeping={{ $outline }}
color_process_zombie={{ $red }}
color_process_stopped={{ $yellow }}
# Header and border colors
color_header={{ $onSurfaceVariant }}
color_border={{ $outline }}
color_separator={{ $outlineVariant }}
# Function key colors
color_function_key={{ $tertiary }}
color_function_desc={{ $onSurface }}
# Tree view colors
color_tree_line={{ $outline }}
color_tree_collapsed={{ $primary }}
color_tree_expanded={{ $secondary }}
# Load average colors
color_load_low={{ $green }}
color_load_med={{ $yellow }}
color_load_high={{ $red }}
# Priority colors
color_priority_high={{ $red }}
color_priority_normal={{ $onSurface }}
color_priority_low={{ $outline }}
# Swap meter colors
color_swap_used={{ $maroon }}
color_swap_cache={{ $peach }}
# Temperature colors (if enabled)
color_temp_cool={{ $green }}
color_temp_warm={{ $yellow }}
color_temp_hot={{ $red }}
+53
View File
@@ -0,0 +1,53 @@
# NVTOP Color Configuration Template
# Format: color_name = RGB_HEX_VALUE
# Colors must be specified as 6-digit hex values without # prefix
# Background colors
background = {{ $surface }}
selected_bg = {{ $surfaceContainer }}
header_bg = {{ $surfaceVariant }}
# Text colors
text = {{ $onSurface }}
selected_text = {{ $primary }}
header_text = {{ $onSurfaceVariant }}
inactive_text = {{ $outline }}
# GPU utilization colors (gradient from low to high)
gpu_util_low = {{ $green }}
gpu_util_med = {{ $yellow }}
gpu_util_high = {{ $red }}
# Memory usage colors
memory_low = {{ $teal }}
memory_med = {{ $sapphire }}
memory_high = {{ $blue }}
# Temperature colors (cool to hot)
temp_cool = {{ $green }}
temp_warm = {{ $yellow }}
temp_hot = {{ $red }}
# Power usage colors
power_low = {{ $green }}
power_med = {{ $peach }}
power_high = {{ $maroon }}
# Process list colors
process_normal = {{ $onSurface }}
process_highlight = {{ $primary }}
process_killed = {{ $red }}
# Border and separator colors
border = {{ $outline }}
separator = {{ $outlineVariant }}
# Chart and graph colors
chart_line = {{ $tertiary }}
chart_fill = {{ $surfaceContainer }}
# Status indicators
status_ok = {{ $green }}
status_warning = {{ $yellow }}
status_error = {{ $red }}
status_info = {{ $blue }}
+162
View File
@@ -0,0 +1,162 @@
{
"$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json",
"name": "Caelestia",
"author": "Unrectified",
"url": "https://github.com/caelestia-dots/cli",
"themes": [
{
"name": "Caelestia",
"mode": "{{ $mode }}",
"colors": {
"accent.background": "{{ $surfaceContainerHigh }}",
"accent.foreground": "{{ $onSurface }}",
"background": "{{ $background }}",
"border": "{{ $outlineVariant }}",
"danger.background": "{{ $error }}",
"foreground": "{{ $onBackground }}",
"input.border": "{{ $outline }}",
"link.active.foreground": "{{ $primary }}",
"link.foreground": "{{ $primary }}",
"link.hover.foreground": "{{ $primaryFixed }}",
"list.active.background": "{{ $secondaryContainer }}",
"list.active.border": "{{ $secondary }}",
"list.even.background": "{{ $surfaceContainerLowest }}",
"muted.background": "{{ $surfaceVariant }}",
"muted.foreground": "{{ $onSurfaceVariant }}",
"panel.background": "{{ $surfaceContainer }}",
"popover.background": "{{ $surfaceContainerHigh }}",
"popover.foreground": "{{ $onSurface }}",
"primary.active.background": "{{ $primaryFixedDim }}",
"primary.background": "{{ $primary }}",
"primary.foreground": "{{ $onPrimary }}",
"primary.hover.background": "{{ $primaryFixed }}",
"scrollbar.background": "{{ $surface }}",
"scrollbar.thumb.background": "{{ $outline }}",
"secondary.background": "{{ $secondaryContainer }}",
"secondary.active.background": "{{ $secondaryFixedDim }}",
"secondary.foreground": "{{ $onSecondary }}",
"secondary.hover.background": "{{ $secondaryFixed }}",
"tab.active.background": "{{ $surface }}",
"tab.active.foreground": "{{ $onSurface }}",
"tab.background": "{{ $surfaceContainerLowest }}",
"tab.foreground": "{{ $onSurfaceVariant }}",
"tab_bar.background": "{{ $surface }}",
"table.background": "{{ $surfaceContainer }}",
"table.head.foreground": "{{ $onSurfaceVariant }}",
"table.row.border": "{{ $outlineVariant }}",
"title_bar.background": "{{ $surfaceDim }}",
"ring": "{{ $primary }}",
"base.red": "{{ $red }}",
"base.red.light": "{{ $peach }}",
"base.green": "{{ $green }}",
"base.green.light": "{{ $teal }}",
"base.blue": "{{ $blue }}",
"base.blue.light": "{{ $sky }}",
"base.cyan": "{{ $teal }}",
"base.cyan.light": "{{ $sky }}",
"base.magenta": "{{ $mauve }}",
"base.magenta.light": "{{ $pink }}",
"base.yellow": "{{ $yellow }}",
"base.yellow.light": "{{ $peach }}"
},
"highlight": {
"editor.foreground": "{{ $onSurface }}",
"editor.background": "{{ $surface }}",
"editor.active_line.background": "{{ $surfaceContainerLow }}",
"editor.line_number": "{{ $onSurfaceVariant }}",
"editor.active_line_number": "{{ $onSurface }}",
"editor.invisible": "{{ $outlineVariant }}",
"conflict": "{{ $red }}",
"created": "{{ $green }}",
"deleted": "{{ $red }}",
"error": "{{ $error }}",
"hidden": "{{ $outline }}",
"hint": "{{ $success }}",
"ignored": "{{ $outline }}",
"info": "{{ $blue }}",
"modified": "{{ $yellow }}",
"predictive": "{{ $overlay1 }}",
"renamed": "{{ $green }}",
"success": "{{ $success }}",
"unreachable": "{{ $outlineVariant }}",
"warning": "{{ $yellow }}",
"syntax": {
"attribute": {
"color": "{{ $yellow }}"
},
"boolean": {
"color": "{{ $green }}"
},
"comment": {
"color": "{{ $subtext0 }}",
"font_style": "italic"
},
"comment.doc": {
"color": "{{ $subtext0 }}",
"font_style": "italic"
},
"constant": {
"color": "{{ $red }}"
},
"constructor": {
"color": "{{ $yellow }}"
},
"embedded": {
"color": "{{ $onSurface }}"
},
"function": {
"color": "{{ $green }}"
},
"keyword": {
"color": "{{ $mauve }}"
},
"link_text": {
"color": "{{ $sky }}",
"font_style": "normal"
},
"link_uri": {
"color": "{{ $klink }}",
"font_style": "italic"
},
"number": {
"color": "{{ $red }}"
},
"string": {
"color": "{{ $green }}"
},
"string.escape": {
"color": "{{ $green }}"
},
"string.regex": {
"color": "{{ $green }}"
},
"string.special": {
"color": "{{ $yellow }}"
},
"string.special.symbol": {
"color": "{{ $yellow }}"
},
"tag": {
"color": "{{ $yellow }}"
},
"text.literal": {
"color": "{{ $red }}"
},
"title": {
"color": "{{ $sky }}",
"font_weight": 600
},
"type": {
"color": "{{ $yellow }}"
},
"property": {
"color": "{{ $onSurface }}"
},
"variable.special": {
"color": "{{ $red }}"
}
}
}
}
]
}
@@ -1,4 +0,0 @@
[ColorScheme]
active_colors = {{ $onSurface }}, {{ $surfaceContainer }}, {{ $surfaceContainerHighest }}, {{ $surfaceContainerHigh }}, {{ $surfaceContainerLowest }}, {{ $surfaceContainerLow }}, {{ $onSurface }}, {{ $onSurface }}, {{ $onSurface }}, {{ $surface }}, {{ $surfaceContainer }}, {{ $shadow }}, {{ $primaryContainer }}, {{ $onPrimaryContainer }}, {{ $secondary }}, {{ $primary }}, {{ $surface }}, {{ $scrim }}, {{ $surfaceContainer }}, {{ $onSurface }}, {{ $secondary }}
inactive_colors = {{ $onSurface }}, {{ $surfaceContainer }}, {{ $surfaceContainerHighest }}, {{ $surfaceContainerHigh }}, {{ $surfaceContainerLowest }}, {{ $surfaceContainerLow }}, {{ $onSurface }}, {{ $onSurface }}, {{ $onSurface }}, {{ $surface }}, {{ $surfaceContainer }}, {{ $shadow }}, {{ $primaryContainer }}, {{ $onPrimaryContainer }}, {{ $secondary }}, {{ $primary }}, {{ $surface }}, {{ $scrim }}, {{ $surfaceContainer }}, {{ $onSurface }}, {{ $secondary }}
disabled_colors = {{ $outline }}, {{ $surface }}, {{ $surfaceContainerHigh }}, {{ $surfaceContainer }}, {{ $surfaceContainerLow }}, {{ $surfaceContainer }}, {{ $outline }}, {{ $onSurfaceVariant }}, {{ $onSurfaceVariant }}, {{ $surface }}, {{ $surfaceContainer }}, {{ $shadow }}, {{ $surfaceContainerHigh }}, {{ $onSurface }}, {{ $onSurfaceVariant }}, {{ $onSurface }}, {{ $surface }}, {{ $scrim }}, {{ $surfaceContainer }}, {{ $onSurface }}, {{ $secondary }}
-6
View File
@@ -1,6 +0,0 @@
[Appearance]
color_scheme_path={{ $config }}/colors/caelestia.conf
custom_palette=true
icon_theme=Papirus-{{ $mode }}
standard_dialogs=default
style=Fusion
+149
View File
@@ -0,0 +1,149 @@
[ColorEffects:Disabled]
Color={{ $surfaceContainer }}
ColorAmount=0.5
ColorEffect=3
ContrastAmount=0
ContrastEffect=0
IntensityAmount=0
IntensityEffect=0
[ColorEffects:Inactive]
ChangeSelectionColor=true
Color={{ $surfaceContainerLowest }}
ColorAmount=0.025
ColorEffect=0
ContrastAmount=0.1
ContrastEffect=0
Enable=true
IntensityAmount=0
IntensityEffect=0
[Colors:Button]
BackgroundAlternate={{ $surfaceVariant }}
BackgroundNormal={{ $surfaceContainerHigh }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $onSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurface }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Complementary]
BackgroundAlternate={{ $surface }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Header]
BackgroundAlternate={{ $surfaceContainer }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Header][Inactive]
BackgroundAlternate={{ $surfaceContainer }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Selection]
BackgroundAlternate={{ $primary }}
BackgroundNormal={{ $primary }}
DecorationFocus={{ $primary }}
DecorationHover={{ $secondary }}
ForegroundActive={{ $onPrimary }}
ForegroundInactive={{ $onPrimary }}
ForegroundLink={{ $klinkSelection }}
ForegroundNegative={{ $knegativeSelection }}
ForegroundNeutral={{ $kneutralSelection }}
ForegroundNormal={{ $onPrimary }}
ForegroundPositive={{ $kpositiveSelection }}
ForegroundVisited={{ $kvisitedSelection }}
[Colors:Tooltip]
BackgroundAlternate={{ $surfaceVariant }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $onSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurface }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:View]
BackgroundAlternate={{ $surfaceContainer }}
BackgroundNormal={{ $surfaceDim }}
DecorationFocus={{ $primary }}
#-----------------------------------------------
DecorationHover={{ $inversePrimary }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurface }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Window]
BackgroundAlternate={{ $surfaceVariant }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $klink }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
#--- Window titles, context icons
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[General]
ColorScheme=Caelestia
Name=Caelestia
shadeSortColumn=true
[KDE]
contrast=4
[WM]
activeBackground={{ $surfaceContainerHighest }}
activeBlend=252,252,252
activeForeground={{ $onSurface }}
inactiveBackground={{ $secondaryContainer }}
inactiveBlend=161,169,177
inactiveForeground={{ $onSecondaryContainer }}
@@ -0,0 +1,22 @@
{
"theme": {
"colorScheme": "~/.config/qtengine/caelestia.colors",
"iconTheme": "Papirus-{{ $mode }}",
"style": "Darkly",
"font": {
"family": "Sans Serif",
"size": 12,
"weight": -1
},
"fontFixed": {
"family": "Monospace",
"size": 12,
"weight": -1
}
},
"misc": {
"menusHaveIcons": true,
"singleClickActivate": false,
"shortcutsForContextMenus": true
}
}
+149
View File
@@ -0,0 +1,149 @@
[ColorEffects:Disabled]
Color={{ $surfaceContainer }}
ColorAmount=0.5
ColorEffect=3
ContrastAmount=0
ContrastEffect=0
IntensityAmount=0
IntensityEffect=0
[ColorEffects:Inactive]
ChangeSelectionColor=true
Color={{ $surfaceContainerLowest }}
ColorAmount=0.025
ColorEffect=0
ContrastAmount=0.1
ContrastEffect=0
Enable=false
IntensityAmount=0
IntensityEffect=0
[Colors:Button]
BackgroundAlternate={{ $surfaceVariant }}
BackgroundNormal={{ $surfaceContainerHigh }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $onSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurface }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Complementary]
BackgroundAlternate={{ $surface }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Header]
BackgroundAlternate={{ $surfaceContainer }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Header][Inactive]
BackgroundAlternate={{ $surfaceContainer }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Selection]
BackgroundAlternate={{ $primary }}
BackgroundNormal={{ $primary }}
DecorationFocus={{ $primary }}
DecorationHover={{ $secondary }}
ForegroundActive={{ $onPrimary }}
ForegroundInactive={{ $onPrimary }}
ForegroundLink={{ $klinkSelection }}
ForegroundNegative={{ $knegativeSelection }}
ForegroundNeutral={{ $kneutralSelection }}
ForegroundNormal={{ $onPrimary }}
ForegroundPositive={{ $kpositiveSelection }}
ForegroundVisited={{ $kvisitedSelection }}
[Colors:Tooltip]
BackgroundAlternate={{ $surfaceVariant }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $onSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurface }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:View]
BackgroundAlternate={{ $surfaceContainer }}
BackgroundNormal={{ $surfaceBright }}
DecorationFocus={{ $primary }}
#-----------------------------------------------
DecorationHover={{ $secondaryFixed }}
ForegroundActive={{ $inverseSurface }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
ForegroundNormal={{ $onSurface }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[Colors:Window]
BackgroundAlternate={{ $surfaceVariant }}
BackgroundNormal={{ $surfaceContainer }}
DecorationFocus={{ $primary }}
DecorationHover={{ $primary }}
ForegroundActive={{ $klink }}
ForegroundInactive={{ $outline }}
ForegroundLink={{ $klink }}
ForegroundNegative={{ $knegative }}
ForegroundNeutral={{ $kneutral }}
#--- Window titles, context icons
ForegroundNormal={{ $onSurfaceVariant }}
ForegroundPositive={{ $kpositive }}
ForegroundVisited={{ $kvisited }}
[General]
ColorScheme=Caelestia
Name=Caelestia
shadeSortColumn=true
[KDE]
contrast=4
[WM]
activeBackground={{ $surfaceContainerHighest }}
activeBlend=227,229,231
activeForeground={{ $onSurface }}
inactiveBackground={{ $secondaryContainer }}
inactiveBlend=239,240,241
inactiveForeground={{ $onSurfaceVariant }}
+202
View File
@@ -0,0 +1,202 @@
/* Thunar theme */
/* =============================================================================
Global Resets
============================================================================= */
.thunar * {
outline: none;
border: none;
}
/* =============================================================================
Window & Background
============================================================================= */
.thunar.background {
background: {{ $surface }};
}
.thunar .titlebar {
background: inherit;
}
.thunar .titlebutton.close {
margin: 0 15px 0 0;
}
/* =============================================================================
Layout Containers
============================================================================= */
/* Paned separator between sidebar and main view */
.thunar paned > separator {
min-width: 4px;
margin-right: -7px;
margin-left: -7px;
background: none;
background-image: none;
box-shadow: none;
}
/* Main file view frame */
.thunar .frame.standard-view {
padding: 10px;
margin: 10px 15px 0 0;
border-radius: 15px;
background-color: {{ $surfaceContainerLow }};
animation: fading 400ms ease forwards;
opacity: 0;
animation-delay: 250ms;
}
.thunar .frame.standard-view .view:not(.rubberband),
.thunar .frame.standard-view .view *:not(.rubberband) {
background-color: transparent;
}
.thunar .frame.standard-view .view *:selected {
color: {{ $primary }};
}
.thunar .rubberband {
background-color: alpha({{ $primary }}, 0.15);
border: 1px solid alpha({{ $primary }}, 0.15);
}
/* Tabs */
.thunar header.top {
background: none;
padding: 0 10px 0 0;
margin: 3px 0 -3px -2px;
}
.thunar header.top tabs .reorderable-page {
margin: 0;
transition: all ease 300ms;
}
.thunar header.top tabs .reorderable-page + .reorderable-page {
margin: 0 0 0 10px;
}
.thunar header.top tabs .reorderable-page:hover {
background-color: alpha({{ $primary }}, 0.08);
}
.thunar header.top tabs .reorderable-page:checked {
color: {{ $primary }};
background-color: alpha({{ $primary }}, 0.15);
}
/* =============================================================================
Sidebar Navigation
============================================================================= */
.thunar .sidebar {
padding: 0 20px;
background: none;
animation: fading 600ms ease forwards;
animation-delay: 100ms;
opacity: 0;
}
.thunar .sidebar .view {
padding: 8px 4px;
border-radius: 10px;
background: none;
transition: all ease 300ms;
}
.thunar .sidebar .view:hover {
background: alpha({{ $onSurface }}, 0.1);
}
.thunar .sidebar .view:selected {
background: alpha({{ $primary }}, 0.15);
color: {{ $primary }};
}
/* =============================================================================
Path Bar & Location Buttons
============================================================================= */
.thunar .path-bar-button {
margin: 0;
padding: 8px 5px;
transition: all ease 0.4s;
}
.thunar .location-button.toggle:checked,
.thunar .path-bar-button.toggle:checked {
padding: 8px 25px;
background: alpha({{ $primary }}, 0.15);
color: {{ $primary }};
box-shadow: none;
}
.thunar .location-button.path-bar-button:not(:checked) {
background-color: {{ $surfaceContainerLow }};
}
.thunar .location-button.path-bar-button:not(:checked):hover {
background: alpha({{ $primary }}, 0.08);
color: alpha({{ $primary }}, 0.8);
}
.thunar .location-button.toggle+.location-button.toggle:checked {
margin-left: 0px;
padding: 0 25px;
}
.thunar .titlebar {
padding: 15px 0 5px 0;
}
/* =============================================================================
Buttons
============================================================================= */
.thunar button.toggle:checked {
color: {{ $primary }};
}
.thunar .image-button {
padding: 8px;
margin: 0 0 0 8px;
transition: all ease 0.4s;
}
/* =============================================================================
Status Bar
============================================================================= */
.thunar statusbar {
background-color: {{ $surfaceContainerLow }};
border-radius: 15px;
padding: 10px 10px;
margin: 15px 5px 15px -10px;
color: {{ $onSurface }};
}
/* =============================================================================
Image preview
============================================================================= */
.thunar box.vertical .image {
margin: 15px;
}
/* =============================================================================
Animation
============================================================================= */
@keyframes fading {
to {
opacity: 1;
}
}
+26
View File
@@ -0,0 +1,26 @@
---
name: 'Caelestia Theme'
accent: '{{ $primary }}'
background: '{{ $surface }}'
foreground: '{{ $onSurface }}'
details: {{ $warp_mode }}
cursor: '{{ $secondary }}'
terminal_colors:
normal:
black: '{{ $term0 }}'
red: '{{ $term1 }}'
green: '{{ $term2 }}'
yellow: '{{ $term3 }}'
blue: '{{ $term4 }}'
magenta: '{{ $term5 }}'
cyan: '{{ $term6 }}'
white: '{{ $term7 }}'
bright:
black: '{{ $term8 }}'
red: '{{ $term9 }}'
green: '{{ $term10 }}'
yellow: '{{ $term11 }}'
blue: '{{ $term12 }}'
magenta: '{{ $term13 }}'
cyan: '{{ $term14 }}'
white: '{{ $term15 }}'
+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
}
}
}
}
]
}
+26 -15
View File
@@ -1,12 +1,12 @@
import argparse
from caelestia.subcommands import clipboard, emoji, pip, record, scheme, screenshot, shell, toggle, wallpaper
from caelestia.subcommands import clipboard, emoji, record, resizer, scheme, screenshot, shell, toggle, wallpaper
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):
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")
@@ -22,19 +22,13 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
shell_parser.add_argument("-d", "--daemon", action="store_true", help="start the shell detached")
shell_parser.add_argument("-s", "--show", action="store_true", help="print all shell IPC commands")
shell_parser.add_argument("-l", "--log", action="store_true", help="print the shell log")
shell_parser.add_argument(
"--log-rules",
default="quickshell.dbus.properties.warning=false;quickshell.dbus.dbusmenu.warning=false;quickshell.service.notifications.warning=false;quickshell.service.sni.host.warning=false;qt.qpa.wayland.textinput.warning=false",
metavar="RULES",
help="log rules to apply",
)
shell_parser.add_argument("-k", "--kill", action="store_true", help="kill the shell")
shell_parser.add_argument("--log-rules", metavar="RULES", help="log rules to apply")
# 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"
)
toggle_parser.add_argument("workspace", help="the workspace to toggle")
# Create parser for scheme opts
scheme_parser = command_parser.add_parser("scheme", help="manage the colour scheme")
@@ -76,6 +70,8 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
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")
record_parser.add_argument("-p", "--pause", action="store_true", help="pause/resume the recording")
record_parser.add_argument("-c", "--clipboard", action="store_true", help="copy recording path to clipboard")
# Create parser for clipboard opts
clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history")
@@ -112,9 +108,24 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
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")
# Create parser for resizer opts
resizer_parser = command_parser.add_parser("resizer", help="window resizer daemon")
resizer_parser.set_defaults(cls=resizer.Command)
resizer_parser.add_argument("-d", "--daemon", action="store_true", help="start the resizer daemon")
resizer_parser.add_argument(
"pattern",
nargs="?",
help="pattern to match against windows ('active' for current window only, 'pip' for quick pip mode)",
)
resizer_parser.add_argument(
"match_type",
nargs="?",
metavar="match_type",
choices=["titleContains", "titleExact", "titleRegex", "initialTitle"],
help="type of pattern matching (titleContains,titleExact,titleRegex,initialTitle)",
)
resizer_parser.add_argument("width", nargs="?", help="width to resize to")
resizer_parser.add_argument("height", nargs="?", help="height to resize to")
resizer_parser.add_argument("actions", nargs="?", help="comma-separated actions to apply (float,center,pip)")
return parser, parser.parse_args()
-44
View File
@@ -1,44 +0,0 @@
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)
+77 -43
View File
@@ -1,10 +1,17 @@
import json
import re
import shutil
import subprocess
import time
from argparse import Namespace
from datetime import datetime
from pathlib import Path
from caelestia.utils.notify import notify
from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir
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
RECORDER = "gpu-screen-recorder"
class Command:
@@ -14,72 +21,99 @@ class Command:
self.args = args
def run(self) -> None:
proc = subprocess.run(["pidof", "wl-screenrec"])
if proc.returncode == 0:
if self.args.pause:
subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL)
elif self.proc_running():
self.stop()
else:
self.start()
def start(self) -> None:
args = []
def proc_running(self) -> bool:
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL).returncode == 0
def intersects(self, a: tuple[int, int, int, int], b: tuple[int, int, int, int]) -> bool:
return a[0] < b[0] + b[2] and a[0] + a[2] > b[0] and a[1] < b[1] + b[3] and a[1] + a[3] > b[1]
def start(self) -> None:
args = ["-w"]
monitors = hypr.message("monitors")
if self.args.region:
if self.args.region == "slurp":
region = subprocess.check_output(["slurp"], text=True)
region = subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True)
else:
region = self.args.region
args += ["-g", region.strip()]
region = self.args.region.strip()
args += ["region", "-region", region]
m = re.match(r"(\d+)x(\d+)\+(\d+)\+(\d+)", region)
if not m:
raise ValueError(f"Invalid region: {region}")
w, h, x, y = map(int, m.groups())
r = x, y, w, h
max_rr = 0
for monitor in monitors:
if self.intersects((monitor["x"], monitor["y"], monitor["width"], monitor["height"]), r):
rr = round(monitor["refreshRate"])
max_rr = max(max_rr, rr)
args += ["-f", str(max_rr)]
else:
focused_monitor = next(monitor for monitor in monitors if monitor["focused"])
if focused_monitor:
args += [focused_monitor["name"], "-f", str(round(focused_monitor["refreshRate"]))]
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")
args += ["-a", "default_output"]
try:
config = json.loads(user_config_path.read_text())
if "record" in config and "extraArgs" in config["record"]:
args += config["record"]["extraArgs"]
except (json.JSONDecodeError, FileNotFoundError):
pass
except TypeError as e:
raise ValueError(f"Config option 'record.extraArgs' should be an array: {e}")
recording_path.parent.mkdir(parents=True, exist_ok=True)
proc = subprocess.Popen(
["wl-screenrec", *args, "--codec", "hevc", "-f", recording_path],
stderr=subprocess.PIPE,
text=True,
start_new_session=True,
)
proc = subprocess.Popen([RECORDER, *args, "-o", str(recording_path)], 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 = notify("-p", "Recording started", "Recording...")
recording_notif_path.write_text(notif)
else:
notify("Recording failed", f"Recording failed to start: {proc.communicate()[1]}")
notif = notify("-p", "Recording started", "Recording...")
recording_notif_path.write_text(notif)
try:
if proc.wait(1) != 0:
close_notification(notif)
notify(
"Recording failed",
"An error occurred attempting to start recorder. "
f"Command `{' '.join(proc.args)}` failed with exit code {proc.returncode}",
)
except subprocess.TimeoutExpired:
pass
def stop(self) -> None:
subprocess.run(["pkill", "wl-screenrec"])
# Start killing recording process
subprocess.run(["pkill", "-f", RECORDER], stdout=subprocess.DEVNULL)
# Wait for recording to finish to avoid corrupted video file
while self.proc_running():
time.sleep(0.1)
# Move to recordings folder
new_path = recordings_dir / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4"
recordings_dir.mkdir(exist_ok=True, parents=True)
recording_path.rename(new_path)
shutil.move(recording_path, 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,
]
)
close_notification(recording_notif_path.read_text())
except IOError:
pass
if self.args.clipboard:
file_uri = Path(new_path).resolve().as_uri() + "\n"
subprocess.run(["wl-copy", "--type", "text/uri-list"], input=file_uri.encode())
action = notify(
"--action=watch=Watch",
"--action=open=Open",
+465
View File
@@ -0,0 +1,465 @@
import json
import re
import socket
import time
from argparse import Namespace
from pathlib import Path
from typing import Any, Dict, Optional
from caelestia.utils import hypr
from caelestia.utils.logging import log_message
from caelestia.utils.paths import user_config_path
class WindowRule:
def __init__(self, name: str, match_type: str, width: str, height: str, actions: list[str]):
self.name = name
self.match_type = match_type
self.width = width
self.height = height
self.actions = actions
class Command:
def __init__(self, args: Namespace) -> None:
self.args = args
self.timeout_tracker: dict[str, float] = {}
self.window_rules = self._load_window_rules()
def _load_window_rules(self) -> list[WindowRule]:
default_rules = [
WindowRule("(Bitwarden", "titleContains", "20%", "54%", ["float", "center"]),
WindowRule("^[Pp]icture(-| )in(-| )[Pp]icture$", "titleRegex", "", "", ["pip"]),
]
try:
config = json.loads(user_config_path.read_text())
if "resizer" in config and "rules" in config["resizer"]:
rules = []
for rule_config in config["resizer"]["rules"]:
rules.append(
WindowRule(
rule_config["name"],
rule_config["matchType"],
rule_config["width"],
rule_config["height"],
rule_config["actions"],
)
)
return rules
except (json.JSONDecodeError, KeyError):
log_message("ERROR: invalid config")
except FileNotFoundError:
pass
return default_rules
def _is_rate_limited(self, key: str) -> bool:
current_time = time.time()
last_time = self.timeout_tracker.get(key, 0)
if current_time < last_time + 1:
return True
self.timeout_tracker[key] = current_time
return False
def _get_window_info(self, window_id: str) -> Optional[Dict[str, Any]]:
try:
clients = hypr.message("clients")
if isinstance(clients, list):
for client in clients:
if isinstance(client, dict) and client.get("address") == f"0x{window_id}":
return client
except Exception:
pass
return None
def _apply_pip_action(self, window_id: str) -> None:
try:
address = f"0x{window_id}"
clients_result = hypr.message("clients")
if not isinstance(clients_result, list):
return
window = None
for c in clients_result:
if isinstance(c, dict) and c.get("address") == address:
window = c
break
if not window or not isinstance(window, dict) or not window.get("floating", False):
return
workspaces_result = hypr.message("workspaces")
if not isinstance(workspaces_result, list):
return
workspace_info = window.get("workspace")
if not isinstance(workspace_info, dict):
return
workspace_name = workspace_info.get("name")
workspace = None
for w in workspaces_result:
if isinstance(w, dict) and w.get("name") == workspace_name:
workspace = w
break
if not workspace or not isinstance(workspace, dict):
return
monitors_result = hypr.message("monitors")
if not isinstance(monitors_result, list):
return
monitor_id = workspace.get("monitorID")
monitor = None
for m in monitors_result:
if isinstance(m, dict) and m.get("id") == monitor_id:
monitor = m
break
if not monitor or not isinstance(monitor, dict):
return
window_size = window.get("size")
if not isinstance(window_size, list) or len(window_size) < 2:
return
width, height = window_size[0], window_size[1]
if not isinstance(width, (int, float)) or not isinstance(height, (int, float)):
return
monitor_height = monitor.get("height")
monitor_width = monitor.get("width")
monitor_scale = monitor.get("scale")
monitor_x = monitor.get("x")
monitor_y = monitor.get("y")
if not all(
isinstance(x, (int, float))
for x in [monitor_height, monitor_width, monitor_scale, monitor_x, monitor_y]
):
return
monitor_height = monitor_height / monitor_scale
monitor_width = monitor_width / monitor_scale
scale_factor = monitor_height / 4 / height
scaled_width = int(width * scale_factor)
scaled_height = int(height * scale_factor)
# Ensure minimum reasonable size
min_width = 200
min_height = 150
scaled_width = max(scaled_width, min_width)
scaled_height = max(scaled_height, min_height)
# Use offset to ensure window stays on screen with some margin
offset = min(monitor_width, monitor_height) * 0.03
# Position in bottom-right corner with offset
move_x = monitor_x + monitor_width - scaled_width - offset
move_y = monitor_y + monitor_height - scaled_height - offset
command1 = f"dispatch resizewindowpixel exact {scaled_width} {scaled_height},address:{address}"
command2 = f"dispatch movewindowpixel exact {int(move_x)} {int(move_y)},address:{address}"
hypr.batch(command1, command2)
log_message(
f"Applied PiP action to window {address}: {scaled_width}x{scaled_height} at ({move_x}, {move_y})"
)
except Exception as e:
log_message(f"ERROR: Failed to apply PiP action to window 0x{window_id}: {e}")
def _apply_window_actions(self, window_id: str, width: str, height: str, actions: list[str]) -> bool:
dispatch_commands = []
if "float" in actions:
window_info = self._get_window_info(window_id)
if window_info and not window_info.get("floating", False):
dispatch_commands.append(f"dispatch togglefloating address:0x{window_id}")
if "pip" in actions:
self._apply_pip_action(window_id)
return True
dispatch_commands.append(f"dispatch resizewindowpixel exact {width} {height},address:0x{window_id}")
if "center" in actions:
dispatch_commands.append("dispatch centerwindow")
try:
hypr.batch(*dispatch_commands)
log_message(f"Applied actions to window 0x{window_id}: {width} x {height} ({', '.join(actions)})")
return True
except Exception as e:
log_message(f"ERROR: Failed to apply window actions for window 0x{window_id}: {e}")
return False
def _match_window_rule(self, window_title: str, initial_title: str) -> WindowRule | None:
for rule in self.window_rules:
if rule.match_type == "initialTitle":
if initial_title == rule.name:
return rule
elif rule.match_type == "titleContains":
if rule.name in window_title:
return rule
elif rule.match_type == "titleExact":
if window_title == rule.name:
return rule
elif rule.match_type == "titleRegex":
try:
if re.search(rule.name, window_title):
return rule
except re.error:
log_message(f"ERROR: Invalid regex pattern in rule '{rule.name}'")
return None
def _handle_window_event(self, event: str) -> None:
if event.startswith("windowtitle"):
self._handle_title_event(event)
elif event.startswith("openwindow"):
self._handle_open_event(event)
def _handle_title_event(self, event: str) -> None:
try:
# Handle both >> and >>> separators (different Hyprland versions)
if ">>>" in event:
window_id = event.split(">>>")[1].split(",")[0]
else:
window_id = event.split(">>")[1].split(",")[0]
# Remove any leading > characters
window_id = window_id.lstrip(">")
if not all(c in "0123456789abcdefABCDEF" for c in window_id):
log_message(f"ERROR: Invalid window ID format: {window_id}")
return
window_info = self._get_window_info(window_id)
if not window_info:
return
window_title = window_info.get("title", "")
initial_title = window_info.get("initialTitle", "")
log_message(f"DEBUG: Window 0x{window_id} - Title: '{window_title}' | Initial: '{initial_title}'")
rule = self._match_window_rule(window_title, initial_title)
if rule:
if self._is_rate_limited(window_id):
log_message(f"Rate limited: skipping window 0x{window_id}")
return
log_message(f"Matched rule '{rule.name}' for window 0x{window_id}")
self._apply_window_actions(window_id, rule.width, rule.height, rule.actions)
except (IndexError, ValueError) as e:
log_message(f"ERROR: Failed to parse window title event: {e}")
def _handle_open_event(self, event: str) -> None:
try:
# Handle both >> and >>> separators
if "openwindow>>>" in event:
data = event[13:] # Remove "openwindow>>>"
else:
data = event[12:] # Remove "openwindow>>"
window_id, workspace, window_class, title = data.split(",", 3)
# Remove any leading > characters
window_id = window_id.lstrip(">")
if not all(c in "0123456789abcdefABCDEF" for c in window_id):
log_message(f"ERROR: Invalid window ID format: {window_id}")
return
log_message(f"DEBUG: New window 0x{window_id} - Title: '{title}' | Class: '{window_class}'")
rule = self._match_window_rule(title, title)
if rule:
if self._is_rate_limited(window_id):
log_message(f"Rate limited: skipping window 0x{window_id}")
return
log_message(f"Matched rule '{rule.name}' for new window 0x{window_id}")
self._apply_window_actions(window_id, rule.width, rule.height, rule.actions)
except (IndexError, ValueError) as e:
log_message(f"ERROR: Failed to parse window open event: {e}")
def run(self) -> None:
if self.args.daemon:
self._run_daemon()
elif hasattr(self.args, "pattern") and self.args.pattern == "pip":
self._run_pip_mode()
elif all(
hasattr(self.args, attr) and getattr(self.args, attr)
for attr in ["pattern", "match_type", "width", "height", "actions"]
):
self._run_active_mode()
else:
print(
"Resizer daemon - use --daemon to start, 'pip' for quick pip mode, or provide pattern, match_type, width, height, and actions for active mode"
)
def _run_pip_mode(self) -> None:
"""Quick pip mode - applies pip action to the active window if it's floating"""
try:
active_window_result = hypr.message("activewindow")
if not isinstance(active_window_result, dict) or not active_window_result.get("address"):
print("ERROR: No active window found")
return
address = active_window_result.get("address", "")
if not isinstance(address, str) or not address.startswith("0x"):
print("ERROR: Invalid window address")
return
window_id = address[2:] # Remove "0x" prefix
window_title = active_window_result.get("title", "")
if not active_window_result.get("floating", False):
print(f"Window '{window_title}' is not floating. PIP only works on floating windows.")
print("Try making it floating first with: hyprctl dispatch togglefloating")
return
print(f"Applying PIP to active window: '{window_title}'")
self._apply_pip_action(window_id)
print("PIP applied successfully")
except Exception as e:
print(f"ERROR: Failed to apply PIP to active window: {e}")
def _run_active_mode(self) -> None:
try:
# Create a temporary rule from command line arguments
actions = self.args.actions.split(",") if self.args.actions else []
temp_rule = WindowRule(self.args.pattern, self.args.match_type, self.args.width, self.args.height, actions)
# Special case: "active" pattern means only target the currently active window
if temp_rule.name.lower() == "active":
self._apply_to_active_window(temp_rule)
return
# Find all windows that match the pattern
matching_windows = self._find_matching_windows(temp_rule)
if not matching_windows:
print(f"No windows found matching pattern '{temp_rule.name}' with match type '{temp_rule.match_type}'")
return
print(f"Found {len(matching_windows)} matching window(s)")
# Apply rule to all matching windows
success_count = 0
for window in matching_windows:
window_id = window["address"][2:] # Remove "0x" prefix
window_title = window.get("title", "")
print(f"Applying rule to window 0x{window_id}: '{window_title}'")
success = self._apply_window_actions(window_id, temp_rule.width, temp_rule.height, temp_rule.actions)
if success:
success_count += 1
print(f"Successfully applied rule to {success_count}/{len(matching_windows)} windows")
except Exception as e:
print(f"ERROR: Failed to apply rule: {e}")
def _apply_to_active_window(self, temp_rule: WindowRule) -> None:
"""Apply rule only to the currently active window"""
try:
active_window_result = hypr.message("activewindow")
if not isinstance(active_window_result, dict) or not active_window_result.get("address"):
print("ERROR: No active window found")
return
window_title = active_window_result.get("title", "")
address = active_window_result.get("address", "")
if not isinstance(address, str) or not address.startswith("0x"):
print("ERROR: Invalid window address")
return
window_id = address[2:] # Remove "0x" prefix
print(f"Applying rule to active window 0x{window_id}: '{window_title}'")
success = self._apply_window_actions(window_id, temp_rule.width, temp_rule.height, temp_rule.actions)
if success:
print("Rule applied successfully")
else:
print("Failed to apply rule")
except Exception as e:
print(f"ERROR: Failed to apply rule to active window: {e}")
def _find_matching_windows(self, temp_rule: WindowRule) -> list:
"""Find all windows that match the given rule pattern"""
try:
clients_result = hypr.message("clients")
if not isinstance(clients_result, list):
return []
matching_windows = []
for window in clients_result:
if not isinstance(window, dict):
continue
window_title = window.get("title", "")
initial_title = window.get("initialTitle", "")
# Check if window matches the pattern
matches = False
if temp_rule.match_type == "initialTitle":
matches = initial_title == temp_rule.name
elif temp_rule.match_type == "titleContains":
matches = temp_rule.name in window_title
elif temp_rule.match_type == "titleExact":
matches = window_title == temp_rule.name
elif temp_rule.match_type == "titleRegex":
try:
matches = bool(re.search(temp_rule.name, window_title))
except re.error:
print(f"ERROR: Invalid regex pattern '{temp_rule.name}'")
return []
if matches:
matching_windows.append(window)
return matching_windows
except Exception as e:
print(f"ERROR: Failed to find matching windows: {e}")
return []
def _run_daemon(self) -> None:
log_message("Hyprland window resizer started")
log_message(f"Loaded {len(self.window_rules)} window rules")
socket_path = Path(hypr.socket2_path)
if not socket_path.exists():
log_message(f"ERROR: Hyprland socket not found at {socket_path}")
return
try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(hypr.socket2_path)
log_message("Connected to Hyprland socket, listening for events...")
while True:
data = sock.recv(4096).decode()
if data:
for line in data.strip().split("\n"):
if line:
self._handle_window_event(line)
except KeyboardInterrupt:
log_message("Resizer daemon stopped")
except Exception as e:
log_message(f"ERROR: {e}")
+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", "-"])
+17 -6
View File
@@ -17,22 +17,30 @@ class Command:
elif self.args.log:
# Print the log
self.print_log()
elif self.args.kill:
# Kill the shell
self.shell("kill")
elif self.args.message:
# Send a message
self.message(*self.args.message)
else:
# Start the shell
args = ["qs", "-c", "caelestia", "-n", "--log-rules", self.args.log_rules]
args = ["qs", "-c", "caelestia", "-n"]
if self.args.log_rules:
args.extend(["--log-rules", self.args.log_rules])
if self.args.daemon:
args.append("-d")
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:
@@ -42,7 +50,10 @@ class Command:
print(self.shell("ipc", "show"), end="")
def print_log(self) -> None:
log = self.shell("log", "-r", self.args.log_rules)
if self.args.log_rules:
log = self.shell("log", "-r", self.args.log_rules)
else:
log = self.shell("log")
# FIXME: remove when logging rules are added/warning is removed
for line in log.splitlines():
if self.filter_log(line):
+140 -51
View File
@@ -1,75 +1,164 @@
import subprocess
import json
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
def is_subset(superset, subset):
for key, value in subset.items():
if key not in superset:
return False
if isinstance(value, dict):
if not is_subset(superset[key], value):
return False
elif isinstance(value, str):
if value not in superset[key]:
return False
elif isinstance(value, list):
if not set(value) <= set(superset[key]):
return False
elif isinstance(value, set):
if not value <= superset[key]:
return False
else:
if not value == superset[key]:
return False
return True
class DeepChainMap(ChainMap):
def __getitem__(self, key):
values = (mapping[key] for mapping in self.maps if key in mapping)
try:
first = next(values)
except StopIteration:
return self.__missing__(key)
if isinstance(first, dict):
return self.__class__(first, *values)
return first
def __repr__(self):
return repr(dict(self))
class Command:
args: Namespace
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
self.cfg = {
"communication": {
"discord": {
"enable": True,
"match": [{"class": "discord"}],
"command": ["discord"],
"move": True,
},
"whatsapp": {
"enable": True,
"match": [{"class": "whatsapp"}],
"move": True,
},
},
"music": {
"spotify": {
"enable": True,
"match": [{"class": "Spotify"}, {"initialTitle": "Spotify"}, {"initialTitle": "Spotify Free"}],
"command": ["spicetify", "watch", "-s"],
"move": True,
},
"feishin": {
"enable": True,
"match": [{"class": "feishin"}],
"move": True,
},
},
"sysmon": {
"btop": {
"enable": True,
"match": [{"class": "btop", "title": "btop", "workspace": {"name": "special:sysmon"}}],
"command": ["foot", "-a", "btop", "-T", "btop", "fish", "-C", "exec btop"],
},
},
"todo": {
"todoist": {
"enable": True,
"match": [{"class": "Todoist"}],
"command": ["todoist"],
"move": True,
},
},
}
try:
self.cfg = DeepChainMap(json.loads(user_config_path.read_text())["toggles"], self.cfg)
except (FileNotFoundError, json.JSONDecodeError, KeyError):
pass
def run(self) -> None:
getattr(self, self.args.workspace)()
if self.args.workspace == "specialws":
self.specialws()
return
def get_clients(self) -> list[dict[str, any]]:
spawned = False
if self.args.workspace in self.cfg:
for client in self.cfg[self.args.workspace].values():
if "enable" in client and client["enable"] and self.handle_client_config(client):
spawned = True
if not spawned:
hypr.dispatch("togglespecialworkspace", self.args.workspace)
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):
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:
exists = any(selector(client) for client in self.get_clients())
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
else:
return False
if not exists:
subprocess.Popen(["app2unit", "--", *spawn], start_new_session=True)
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):
return True
return False
return not exists
spawned = False
if "command" in client and client["command"]:
spawned = self.spawn_client(selector, client["command"])
if "move" in client and client["move"]:
self.move_client(selector, self.args.workspace)
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", "fish", "-C", "exec btop"],
)
hypr.dispatch("togglespecialworkspace", "sysmon")
def todo(self) -> None:
self.spawn_or_move(lambda c: c["class"] == "Todoist", ["todoist"], "todo")
hypr.dispatch("togglespecialworkspace", "todo")
return spawned
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)
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)
+28
View File
@@ -0,0 +1,28 @@
class Colour:
_rgb_vals: tuple[int, ...]
_hex_vals: tuple[str, ...]
def __init__(self, hex: str):
hex = hex.ljust(8, "f")
self._hex_vals = tuple(hex[i : i + 2] for i in range(0, 7, 2))
self._rgb_vals = tuple(int(h, 16) for h in self._hex_vals)
@property
def hex(self) -> str:
return "".join(self._hex_vals[:-1])
@property
def hexalpha(self) -> str:
return "".join(self._hex_vals)
@property
def rgb(self) -> str:
return f"rgb({','.join(map(str, self._rgb_vals[:-1]))})"
@property
def rgbalpha(self) -> str:
return f"rgba({','.join(map(str, self._rgb_vals))})"
def get_dynamic_colours(colours: dict[str, str]) -> dict[str, Colour]:
return {name: Colour(code) for name, code in colours.items()}
+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:
+16 -6
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,8 +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: 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)
+22
View File
@@ -0,0 +1,22 @@
from time import strftime
def log_message(message: str) -> None:
timestamp = strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] {message}")
def log_exception(func):
"""Log exceptions to stdout instead of raising
Used by the `apply_()` functions so that an exception, when applying
a theme, does not prevent the other themes from being applied.
"""
def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except Exception as e:
log_message(f'Error during execution of "{func.__name__}()": {str(e)}')
return wrapper
+1 -1
View File
@@ -31,7 +31,7 @@ def get_colours_for_image(image: Path | str = wallpaper_thumbnail_path, scheme=N
scheme = get_scheme()
cache_base = scheme_cache_dir / compute_hash(image)
cache = (cache_base / scheme.variant / scheme.mode).with_suffix(".json")
cache = (cache_base / scheme.variant / scheme.flavour / scheme.mode).with_suffix(".json")
try:
with cache.open("r") as f:
+74 -19
View File
@@ -1,8 +1,5 @@
from materialyoucolor.blend import Blend
from materialyoucolor.dynamiccolor.material_dynamic_colors import (
DynamicScheme,
MaterialDynamicColors,
)
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
from materialyoucolor.hct import Hct
from materialyoucolor.scheme.scheme_content import SchemeContent
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive
@@ -14,6 +11,19 @@ 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
except ImportError:
from materialyoucolor.scheme.dynamic_scheme import DynamicScheme
def hex_to_hct(hex: str) -> Hct:
@@ -92,6 +102,14 @@ dark_catppuccin = [
hex_to_hct("b4befe"),
]
kcolours = [
{"name": "klink", "hct": hex_to_hct("2980b9")},
{"name": "kvisited", "hct": hex_to_hct("9b59b6")},
{"name": "knegative", "hct": hex_to_hct("da4453")},
{"name": "kneutral", "hct": hex_to_hct("f67400")},
{"name": "kpositive", "hct": hex_to_hct("27ae60")},
]
colour_names = [
"rosewater",
"flamingo",
@@ -134,10 +152,10 @@ def lighten(colour: Hct, amount: float) -> Hct:
def darken(colour: Hct, amount: float) -> Hct:
diff = colour.tone * amount
return Hct.from_hct(colour.hue, colour.chroma + diff / 5, colour.tone - diff)
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":
@@ -158,37 +176,62 @@ 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)
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)
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:
colours[colour.name] = colour.get_hct(primary_scheme)
else:
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)
# Backwards compatibility with old colour names
if "primaryPaletteKeyColor" in colours: # materialyoucolor-python >= 3.0.0
for colour in "primary", "secondary", "tertiary", "neutral":
colours[f"{colour}_paletteKeyColor"] = colours[f"{colour}PaletteKeyColor"]
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"]], 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():
colours[name].chroma -= 15
# 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 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"]
colours["subtext1"] = colours["onSurfaceVariant"]
@@ -203,13 +246,25 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
colours["mantle"] = darken(colours["surface"], 0.03)
colours["crust"] = darken(colours["surface"], 0.05)
# More darkening if hard flavour
if scheme.flavour == "hard":
for colour in "base", "mantle", "crust":
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 is_light else darken(colours[f"overlay{i}"], 0.8)
)
colours[f"surface{i}"] = (
lighten(colours[f"surface{i}"], 0.4) if is_light else darken(colours[f"surface{i}"], 0.8)
)
# For debugging
# print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()]))
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"
+16 -1
View File
@@ -1,5 +1,20 @@
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()
def close_notification(id: str) -> None:
subprocess.run(
[
"gdbus",
"call",
"--session",
"--dest=org.freedesktop.Notifications",
"--object-path=/org/freedesktop/Notifications",
"--method=org.freedesktop.Notifications.CloseNotification",
id,
],
stdout=subprocess.DEVNULL,
)
+30 -24
View File
@@ -4,36 +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"))
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"
cli_data_dir = Path(__file__).parent.parent / "data"
templates_dir = cli_data_dir / "templates"
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 = 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"
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 = Path.home() / "Pictures/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 = Path.home() / "Videos/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:
@@ -46,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()
+18 -15
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,11 +197,11 @@ scheme_variants = [
"content",
]
scheme: Scheme = None
scheme: Scheme | None = 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())}
return {k.strip(): v.strip() for k, v in (line.split(" ") for line in path.read_text().splitlines() if line)}
def get_scheme_path() -> Path:
@@ -225,18 +226,20 @@ 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
return ["default"] if name == "dynamic" else [f.name for f in (scheme_data_dir / name).iterdir() if f.is_dir()]
return (
["default", "hard"] if name == "dynamic" else [f.name for f in (scheme_data_dir / name).iterdir() if f.is_dir()]
)
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"]
+384 -53
View File
@@ -1,7 +1,24 @@
import fcntl
import json
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from caelestia.utils.paths import c_state_dir, config_dir, templates_dir
from caelestia.utils.colour import get_dynamic_colours
from caelestia.utils.logging import log_exception
from caelestia.utils.paths import (
c_state_dir,
config_dir,
data_dir,
templates_dir,
theme_dir,
user_config_path,
user_templates_dir,
)
from caelestia.utils.scheme import get_scheme
def gen_conf(colours: dict[str, str]) -> str:
@@ -19,13 +36,38 @@ 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 c2s(c: str, *i: list[int]) -> str:
def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> str:
def fill_colour(match: re.Match) -> str:
data = match.group(1).strip().split(".")
if len(data) != 2:
return match.group()
col, form = data
if col not in colours_dyn or not hasattr(colours_dyn[col], form):
return match.group()
return getattr(colours_dyn[col], form)
# match atomic {{ . }} pairs
dotField = r"\{\{((?:(?!\{\{|\}\}).)*)\}\}"
# match {{ mode }}
modeField = r"\{\{\s*mode\s*\}\}"
colours_dyn = get_dynamic_colours(colours)
template_content = template.read_text()
template_filled = re.sub(dotField, fill_colour, template_content)
template_filled = re.sub(modeField, mode, template_filled)
return template_filled
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\\"
@@ -42,37 +84,42 @@ 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)
)
def write_file(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
with tempfile.NamedTemporaryFile("w") as f:
f.write(content)
f.flush()
shutil.move(f.name, path)
@log_exception
def apply_terms(sequences: str) -> None:
state = c_state_dir / "sequences.txt"
state.parent.mkdir(parents=True, exist_ok=True)
@@ -81,14 +128,26 @@ 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)
try:
# Use non-blocking write with timeout to prevent hangs
import os
fd = os.open(str(pt), os.O_WRONLY | os.O_NONBLOCK | os.O_NOCTTY)
try:
os.write(fd, sequences.encode())
finally:
os.close(fd)
except (PermissionError, OSError, BlockingIOError):
# Skip terminals that are busy, closed, or inaccessible
pass
@log_exception
def apply_hypr(conf: str) -> None:
write_file(config_dir / "hypr/scheme/current.conf", conf)
@log_exception
def apply_discord(scss: str) -> None:
import tempfile
@@ -100,51 +159,323 @@ def apply_discord(scss: str) -> None:
write_file(config_dir / client / "themes/caelestia.theme.css", conf)
@log_exception
def apply_pandora(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / "pandora.json", hash=True)
template = template.replace("{{ $mode }}", mode)
write_file(data_dir / "PandoraLauncher/themes/caelestia.json", template)
@log_exception
def apply_spicetify(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / f"spicetify-{mode}.ini")
write_file(config_dir / "spicetify/Themes/caelestia/color.ini", template)
@log_exception
def apply_fuzzel(colours: dict[str, str]) -> None:
template = gen_replace(colours, templates_dir / "fuzzel.ini")
write_file(config_dir / "fuzzel/fuzzel.ini", template)
@log_exception
def apply_btop(colours: dict[str, str]) -> None:
template = gen_replace(colours, templates_dir / "btop.theme", hash=True)
write_file(config_dir / "btop/themes/caelestia.theme", template)
subprocess.run(["killall", "-USR2", "btop"], stderr=subprocess.DEVNULL)
def apply_gtk(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / "gtk.css", hash=True)
write_file(config_dir / "gtk-3.0/gtk.css", template)
write_file(config_dir / "gtk-4.0/gtk.css", template)
@log_exception
def apply_nvtop(colours: dict[str, str]) -> None:
template = gen_replace(colours, templates_dir / "nvtop.colors", hash=True)
write_file(config_dir / "nvtop/nvtop.colors", template)
@log_exception
def apply_htop(colours: dict[str, str]) -> None:
template = gen_replace(colours, templates_dir / "htop.theme", hash=True)
write_file(config_dir / "htop/htoprc", template)
subprocess.run(["killall", "-USR2", "htop"], stderr=subprocess.DEVNULL)
def sync_papirus_colors(hex_color: str) -> None:
"""Sync Papirus folder icon colors using hue/saturation analysis"""
try:
result = subprocess.run(["which", "papirus-folders"], capture_output=True, check=False)
if result.returncode != 0:
return
except Exception:
return
papirus_paths = [
Path("/usr/share/icons/Papirus"),
Path("/usr/share/icons/Papirus-Dark"),
Path.home() / ".local/share/icons/Papirus",
Path.home() / ".icons/Papirus",
]
if not any(p.exists() for p in papirus_paths):
return
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
# Brightness and saturation
max_val = max(r, g, b)
min_val = min(r, g, b)
brightness = max_val
saturation = 0 if max_val == 0 else ((max_val - min_val) * 100) // max_val
# Low saturation = grayscale
if saturation < 20:
if brightness < 85:
color = "black"
elif brightness < 170:
color = "grey"
else:
color = "white"
# Medium-low saturation with high brightness = pale variants
elif saturation < 60 and brightness > 180:
use_pale = True
color = _determine_hue_color(r, g, b, brightness, use_pale)
else:
color = _determine_hue_color(r, g, b, brightness, False)
try:
subprocess.Popen(
["sudo", "-n", "papirus-folders", "-C", color, "-u"],
stderr=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
start_new_session=True,
)
except Exception:
pass
def _determine_hue_color(r: int, g: int, b: int, brightness: int, use_pale: bool) -> str:
if b > r and b > g:
# Blue dominant
r_ratio = (r * 100) // b if b > 0 else 0
g_ratio = (g * 100) // b if b > 0 else 0
rg_diff = abs(r - g)
if r_ratio > 70 and g_ratio > 70:
# Both R and G high relative to B = light blue/periwinkle
if rg_diff < 15:
return "blue"
elif r > g:
return "violet"
else:
return "cyan"
elif r_ratio > 60 and r > g:
return "violet"
elif g_ratio > 60 and g > r:
return "cyan"
else:
return "blue"
elif r > g and r > b:
# Red dominant
if g > b + 30:
# Orange/yellow-ish/brown
rg_ratio = (g * 100) // r if r > 0 else 0
if use_pale:
if rg_ratio > 70 and brightness < 220:
return "palebrown"
else:
return "paleorange"
else:
if rg_ratio > 70 and brightness < 180:
return "brown"
else:
return "orange"
elif b > g + 20:
return "pink"
else:
return "pink" if use_pale else "red"
elif g > r and g > b:
# Green dominant
if r > b + 30:
return "yellow"
else:
return "green"
else:
return "grey"
@log_exception
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)
for gtk_version in ["gtk-3.0", "gtk-4.0"]:
gtk_config_dir = config_dir / gtk_version
write_file(gtk_config_dir / "gtk.css", gtk_template)
write_file(gtk_config_dir / "thunar.css", thunar_template)
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"])
def apply_qt(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / "qtcolors.conf", hash=True)
write_file(config_dir / "qt5ct/colors/caelestia.conf", template)
write_file(config_dir / "qt6ct/colors/caelestia.conf", template)
@log_exception
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)
qtct = (templates_dir / "qtct.conf").read_text()
qtct = qtct.replace("{{ $mode }}", mode.capitalize())
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)
for ver in 5, 6:
conf = qtct.replace("{{ $config }}", str(config_dir / f"qt{ver}ct"))
write_file(config_dir / f"qt{ver}ct/qt{ver}ct.conf", conf)
@log_exception
def apply_warp(colours: dict[str, str], mode: str) -> None:
warp_mode = "darker" if mode == "dark" else "lighter"
template = gen_replace(colours, templates_dir / "warp.yaml", hash=True)
template = template.replace("{{ $warp_mode }}", warp_mode)
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)
write_file(config_dir / "cava/config", template)
subprocess.run(["killall", "-USR2", "cava"], stderr=subprocess.DEVNULL)
@log_exception
def apply_user_templates(colours: dict[str, str], mode: str) -> None:
if not user_templates_dir.is_dir():
return
for file in user_templates_dir.iterdir():
if file.is_file():
content = gen_replace_dynamic(colours, file, mode)
write_file(theme_dir / file.name, content)
def apply_colours(colours: dict[str, str], mode: str) -> None:
apply_terms(gen_sequences(colours))
apply_hypr(gen_conf(colours))
apply_discord(gen_scss(colours))
apply_spicetify(colours, mode)
apply_fuzzel(colours)
apply_btop(colours)
apply_gtk(colours, mode)
apply_qt(colours, mode)
# Use file-based lock to prevent concurrent theme changes
lock_file = c_state_dir / "theme.lock"
c_state_dir.mkdir(parents=True, exist_ok=True)
try:
with open(lock_file, "w") as lock_fd:
try:
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
return
try:
cfg = json.loads(user_config_path.read_text())["theme"]
except (FileNotFoundError, json.JSONDecodeError, KeyError):
cfg = {}
def check(key: str) -> bool:
return cfg[key] if key in cfg else True
if check("enableTerm"):
apply_terms(gen_sequences(colours))
if check("enableHypr"):
apply_hypr(gen_conf(colours))
if check("enableDiscord"):
apply_discord(gen_scss(colours))
if check("enableSpicetify"):
apply_spicetify(colours, mode)
if check("enablePandora"):
apply_pandora(colours, mode)
if check("enableFuzzel"):
apply_fuzzel(colours)
if check("enableBtop"):
apply_btop(colours)
if check("enableNvtop"):
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, icon_theme)
if check("enableQt"):
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()
except FileNotFoundError:
pass
+38 -18
View File
@@ -1,30 +1,50 @@
import shutil
import subprocess
from caelestia.utils.paths import config_dir
def print_version() -> None:
print("Packages:")
pkgs = ["caelestia-shell-git", "caelestia-cli-git", "caelestia-meta"]
versions = subprocess.run(
["pacman", "-Q", *pkgs], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
).stdout
if shutil.which("pacman"):
print("Packages:")
pkgs = ["caelestia-shell", "caelestia-cli", "caelestia-meta"]
versions = subprocess.run(
["pacman", "-Q", *pkgs], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
).stdout
for pkg in pkgs:
if pkg not in versions:
print(f" {pkg} not installed")
print("\n".join(f" {pkg}" for pkg in versions.splitlines()))
for pkg in pkgs:
if pkg not in versions:
print(f" {pkg} not installed")
print("\n".join(f" {pkg}" for pkg in versions.splitlines()))
else:
print("Packages: not on Arch")
caelestia_dir = (config_dir / "hypr").resolve().parent
print("\nCaelestia:")
caelestia_ver = subprocess.check_output(
["git", "--git-dir", caelestia_dir / ".git", "rev-list", "--format=%B", "--max-count=1", "HEAD"], text=True
)
print(" Last commit:", caelestia_ver.split()[1])
print(" Commit message:", *caelestia_ver.splitlines()[1:])
print()
try:
caelestia_dir = (config_dir / "hypr").resolve().parent
caelestia_ver = subprocess.check_output(
["git", "--git-dir", caelestia_dir / ".git", "rev-list", "--format=%B", "--max-count=1", "HEAD"], text=True
)
print("Caelestia:")
print(" Last commit:", caelestia_ver.split()[1])
print(" Commit message:", *caelestia_ver.splitlines()[1:])
except subprocess.CalledProcessError:
print("Caelestia: not installed")
print("\nQuickshell:")
print(" ", subprocess.check_output(["qs", "--version"], text=True).strip())
print()
try:
shell_ver = subprocess.check_output(["/usr/lib/caelestia/version", "-s"], text=True).strip()
print("Shell:")
print(" ", shell_ver)
except FileNotFoundError:
print("Shell: version helper not available")
print()
if shutil.which("qs"):
print("Quickshell:")
print(" ", subprocess.check_output(["qs", "--version"], text=True).strip())
else:
print("Quickshell: not in PATH")
local_shell_dir = config_dir / "quickshell/caelestia"
if local_shell_dir.exists():
+88 -25
View File
@@ -1,7 +1,11 @@
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
@@ -9,8 +13,10 @@ 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,
wallpaper_link_path,
wallpaper_path_path,
wallpaper_thumbnail_path,
@@ -21,7 +27,7 @@ from caelestia.utils.theme import apply_colours
def is_valid_image(path: Path) -> bool:
return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"]
return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff", ".gif"]
def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bool:
@@ -30,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:
@@ -38,22 +44,17 @@ 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")
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"]
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)]
@@ -64,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:
@@ -79,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)
@@ -98,9 +100,13 @@ def get_smart_opts(wall: Path, cache: Path) -> str:
def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
wall = Path(wall)
scheme = get_scheme()
cache = wallpapers_cache_dir / compute_hash(wall)
if wall.suffix.lower() == ".gif":
wall = convert_gif(wall)
name = "dynamic"
if not no_smart:
@@ -108,7 +114,7 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
scheme = Scheme(
{
"name": name,
"flavour": "default",
"flavour": scheme.flavour,
"mode": smart_opts["mode"],
"variant": smart_opts["variant"],
"colours": scheme.colours,
@@ -117,20 +123,41 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
return {
"name": name,
"flavour": "default",
"flavour": scheme.flavour,
"mode": scheme.mode,
"variant": scheme.variant,
"colours": get_colours_for_image(get_thumb(wall, cache), scheme),
}
def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
def convert_gif(wall: Path) -> Path:
cache = wallpapers_cache_dir / compute_hash(wall)
output_path = cache / "first_frame.png"
if not output_path.exists():
output_path.parent.mkdir(parents=True, exist_ok=True)
with Image.open(wall) as img:
try:
img.seek(0)
except EOFError:
pass
img = img.convert("RGB")
img.save(output_path, "PNG")
return output_path
def set_wallpaper(wall: Path, no_smart: bool) -> None:
# Make path absolute
wall = Path(wall).resolve()
if not is_valid_image(wall):
raise ValueError(f'"{wall}" is not a valid image')
# Use gif's 1st frame for thumb only
wall_cache = convert_gif(wall) if wall.suffix.lower() == ".gif" else wall
# Update files
wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True)
wallpaper_path_path.write_text(str(wall))
@@ -138,10 +165,10 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
wallpaper_link_path.unlink(missing_ok=True)
wallpaper_link_path.symlink_to(wall)
cache = wallpapers_cache_dir / compute_hash(wall)
cache = wallpapers_cache_dir / compute_hash(wall_cache)
# Generate thumbnail or get from cache
thumb = get_thumb(wall, cache)
thumb = get_thumb(wall_cache, cache)
wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
wallpaper_thumbnail_path.unlink(missing_ok=True)
wallpaper_thumbnail_path.symlink_to(thumb)
@@ -150,7 +177,7 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
# Change mode and variant based on wallpaper colour
if scheme.name == "dynamic" and not no_smart:
smart_opts = get_smart_opts(wall, cache)
smart_opts = get_smart_opts(wall_cache, cache)
scheme.mode = smart_opts["mode"]
scheme.variant = smart_opts["variant"]
@@ -158,6 +185,42 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
scheme.update_colours()
apply_colours(scheme.colours, scheme.mode)
# Run custom post-hook if configured
try:
cfg = json.loads(user_config_path.read_text()).get("wallpaper", {})
if post_hook := cfg.get("postHook"):
subprocess.run(
post_hook,
shell=True,
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):
pass
def set_random(args: Namespace) -> None:
set_wallpaper(random.choice(get_wallpapers(args)), args.no_smart)
wallpapers = get_wallpapers(args)
if not wallpapers:
raise ValueError("No valid wallpapers found")
try:
last_wall = wallpaper_path_path.read_text()
wallpapers.remove(Path(last_wall))
if not wallpapers:
raise ValueError("Only valid wallpaper is current")
except (FileNotFoundError, ValueError):
pass
set_wallpaper(random.choice(wallpapers), args.no_smart)