81 Commits

Author SHA1 Message Date
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
57 changed files with 2292 additions and 299 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 name: Update emojis
on: on:
push:
workflow_dispatch: workflow_dispatch:
schedule:
- cron: "0 0 * * 0"
jobs: jobs:
update: update:
@@ -28,11 +28,11 @@ jobs:
pip install . pip install .
- name: Fetch emojis - name: Fetch emojis
run: ./run.sh emoji -f run: ./bin/caelestia emoji -f
- name: Check for changes - name: Check for changes
id: check 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 - name: Commit and push changes
if: steps.check.outputs.modified == 'true' 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"
+121 -21
View File
@@ -11,9 +11,8 @@ The main control script for the Caelestia dotfiles.
- [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps - [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps
- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard - [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard
- [`slurp`](https://github.com/emersion/slurp) - selecting an area - [`slurp`](https://github.com/emersion/slurp) - selecting an area
- [`wl-screenrec`](https://github.com/russelltg/wl-screenrec) - screen recording - [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording
- `glib2` - closing notifications - `glib2` - closing notifications
- `libpulse` - getting audio device
- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history - [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history
- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker - [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
@@ -21,18 +20,52 @@ The main control script for the Caelestia dotfiles.
## Installation ## Installation
### Package manager (recommended) ### Arch linux
The cli is available from the AUR as `caelestia-cli-git`. To install it you can use The CLI is available from the AUR as `caelestia-cli`. You can install it with an AUR helper
an AUR helper like [`yay`](https://github.com/Jguer/yay), or manually download the like [`yay`](https://github.com/Jguer/yay) or manually downloading the PKGBUILD and running `makepkg -si`.
PKGBUILD and run `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 ```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 ### Manual installation
Install all [dependencies](#dependencies), then install Install all [dependencies](#dependencies), then install
@@ -44,7 +77,7 @@ Install all [dependencies](#dependencies), then install
e.g. via an AUR helper (yay) e.g. via an AUR helper (yay)
```sh ```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` Now, clone the repo, `cd` into it, build the wheel via `python -m build --wheel`
@@ -66,24 +99,91 @@ All subcommands/options can be explored via the help flag.
``` ```
$ caelestia -h $ caelestia -h
usage: caelestia [-h] COMMAND ... usage: caelestia [-h] [-v] COMMAND ...
Main control script for the Caelestia dotfiles Main control script for the Caelestia dotfiles
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --version print the current version
subcommands: subcommands:
valid subcommands valid subcommands
COMMAND the subcommand to run COMMAND the subcommand to run
shell start or message the shell shell start or message the shell
toggle toggle a special workspace toggle toggle a special workspace
scheme manage the colour scheme scheme manage the colour scheme
screenshot take a screenshot screenshot take a screenshot
record start a screen recording record start a screen recording
clipboard open clipboard history clipboard open clipboard history
emoji emoji/glyph utilities emoji emoji/glyph utilities
wallpaper manage the wallpaper wallpaper manage the wallpaper
pip picture in picture utilities resizer window resizer daemon
``` ```
## Configuring
All configuration options are in `~/.config/caelestia/cli.json`.
<details><summary>Example configuration</summary>
```json
{
"theme": {
"enableTerm": true,
"enableHypr": true,
"enableDiscord": true,
"enableSpicetify": true,
"enableFuzzel": true,
"enableBtop": true,
"enableGtk": true,
"enableQt": true
},
"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 # Utility script for running caelestia
cd "$(dirname $0)/src" || exit cd "$(dirname $0)/../src" || exit
python -m caelestia "$@" python -m caelestia "$@"
+7 -5
View File
@@ -1,7 +1,7 @@
set -l seen '__fish_seen_subcommand_from' set -l seen '__fish_seen_subcommand_from'
set -l has_opt '__fish_contains_opt' 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" set -l not_seen "not $seen $commands"
# Disable file completions # 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 'clipboard' -d 'Open clipboard history'
complete -c caelestia -n $not_seen -a 'emoji' -d 'Emoji/glyph utilities' 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 '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 # Shell
set -l commands mpris drawers wallpaper notifs 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 'list' -d 'List togglable drawers'
complete -c caelestia -n $not_seen -a 'toggle' -d 'Toggle a drawer' 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' complete -c caelestia -n "$seen shell && $seen drawers && $seen toggle && not $seen $commands" -a "$commands" -d 'drawer'
set -l commands list get set set -l commands list get set
@@ -121,5 +121,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 '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' complete -c caelestia -n "$seen emoji" -s 'f' -l 'fetch' -d 'Fetch emoji/glyph data from remote'
# Pip # Resizer
complete -c caelestia -n "$seen pip" -s 'd' -l 'daemon' -d 'Start in daemon mode' 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'
+5 -5
View File
@@ -12,13 +12,13 @@
dart-sass, dart-sass,
grim, grim,
fuzzel, fuzzel,
wl-screenrec, gpu-screen-recorder,
dconf, dconf,
killall, killall,
caelestia-shell, caelestia-shell,
withShell ? false, withShell ? false,
discordBin ? "discord", discordBin ? "discord",
qtctStyle ? "Fusion", qtctStyle ? "Darkly",
}: }:
python3.pkgs.buildPythonApplication { python3.pkgs.buildPythonApplication {
pname = "caelestia-cli"; pname = "caelestia-cli";
@@ -50,7 +50,7 @@ python3.pkgs.buildPythonApplication {
dart-sass dart-sass
grim grim
fuzzel fuzzel
wl-screenrec gpu-screen-recorder
dconf dconf
killall killall
] ]
@@ -70,9 +70,9 @@ python3.pkgs.buildPythonApplication {
--replace-fail 'discord' ${discordBin} \ --replace-fail 'discord' ${discordBin} \
--replace-fail 'todoist' 'todoist.desktop' --replace-fail 'todoist' 'todoist.desktop'
# Use config style instead of fusion # Use config style instead of darkly
substituteInPlace src/caelestia/data/templates/qtct.conf \ substituteInPlace src/caelestia/data/templates/qtct.conf \
--replace-fail 'Fusion' '${qtctStyle}' --replace-fail 'Darkly' '${qtctStyle}'
''; '';
postInstall = "installShellCompletion completions/caelestia.fish"; postInstall = "installShellCompletion completions/caelestia.fish";
Generated
+11 -60
View File
@@ -1,67 +1,19 @@
{ {
"nodes": { "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": { "caelestia-shell": {
"inputs": { "inputs": {
"app2unit": [ "caelestia-cli": [],
"app2unit"
],
"caelestia-cli": "caelestia-cli",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ],
"quickshell": "quickshell" "quickshell": "quickshell"
}, },
"locked": { "locked": {
"lastModified": 1752637099, "lastModified": 1757726711,
"narHash": "sha256-08oPnEGYkuU7Vqa4F7rOi4E9j2Drigm3DxdOA+/mgF4=", "narHash": "sha256-nihMIyW+IN01jLH+XhRDJ4V/9ulD/iqi0dvA7gYlclA=",
"owner": "caelestia-dots", "owner": "caelestia-dots",
"repo": "shell", "repo": "shell",
"rev": "19431534c954f763eb095dd131fd0b19ff74837b", "rev": "a57dd9343a2643f73f3994dc230b824617f89ecf",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -72,11 +24,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1752480373, "lastModified": 1757487488,
"narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=", "narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08", "rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -94,11 +46,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1752631407, "lastModified": 1756981260,
"narHash": "sha256-dLDtKxh1VabwLxv5xbjI+oRkDyqWEKGITU+0dEaaW28=", "narHash": "sha256-GhuD9QVimjynHI0OOyZsqJsnlXr2orowh9H+HYz4YMs=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "4d8055f1cd9924bcace59405894b8879633eb83d", "rev": "6eb12551baf924f8fdecdd04113863a754259c34",
"revCount": 638, "revCount": 672,
"type": "git", "type": "git",
"url": "https://git.outfoxxed.me/outfoxxed/quickshell" "url": "https://git.outfoxxed.me/outfoxxed/quickshell"
}, },
@@ -109,7 +61,6 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"app2unit": "app2unit",
"caelestia-shell": "caelestia-shell", "caelestia-shell": "caelestia-shell",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
+3 -13
View File
@@ -4,15 +4,10 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
app2unit = {
url = "github:soramanew/app2unit";
inputs.nixpkgs.follows = "nixpkgs";
};
caelestia-shell = { caelestia-shell = {
url = "github:caelestia-dots/shell"; url = "github:caelestia-dots/shell";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.app2unit.follows = "app2unit"; inputs.caelestia-cli.follows = "";
}; };
}; };
@@ -31,20 +26,15 @@
packages = forAllSystems (pkgs: rec { packages = forAllSystems (pkgs: rec {
caelestia-cli = pkgs.callPackage ./default.nix { caelestia-cli = pkgs.callPackage ./default.nix {
rev = self.rev or self.dirtyRev; rev = self.rev or self.dirtyRev;
app2unit = inputs.app2unit.packages.${pkgs.system}.default;
caelestia-shell = inputs.caelestia-shell.packages.${pkgs.system}.default; caelestia-shell = inputs.caelestia-shell.packages.${pkgs.system}.default;
}; };
with-shell = caelestia-cli.override {withShell = true;};
default = caelestia-cli; default = caelestia-cli;
}); });
devShells = forAllSystems (pkgs: { devShells = forAllSystems (pkgs: {
default = pkgs.mkShellNoCC { default = pkgs.mkShellNoCC {
inputsFrom = [self.packages.${pkgs.system}.caelestia-cli]; packages = [self.packages.${pkgs.system}.with-shell];
packages = [
(pkgs.writeShellScriptBin "caelestia" ''
cd src && python -m caelestia "$@"
'')
];
}; };
}); });
}; };
+10
View File
@@ -16,3 +16,13 @@ caelestia = "caelestia:main"
[tool.hatch.version] [tool.hatch.version]
source = "vcs" source = "vcs"
[tool.hatch.build.targets.sdist]
only-include = [
"src",
"completions",
"README.md"
]
[tool.ruff]
line-length = 120
@@ -82,6 +82,16 @@ sky cadcff
sapphire aec7ff sapphire aec7ff
blue a6baff blue a6baff
lavender bfcaff lavender bfcaff
klink 6685d1
klinkSelection 6585d1
kvisited 7276dd
kvisitedSelection 7276dd
knegative 8e70ff
knegativeSelection 8e70ff
kneutral c794ff
kneutralSelection c794ff
kpositive 54afff
kpositiveSelection 54afff
text e4e1e7 text e4e1e7
subtext1 c6c5d1 subtext1 c6c5d1
subtext0 8f909a subtext0 8f909a
@@ -82,6 +82,16 @@ sky 0082b6
sapphire 037ba6 sapphire 037ba6
blue 005e90 blue 005e90
lavender 0077b7 lavender 0077b7
klink 2e8fc3
klinkSelection 308fc4
kvisited 2584d6
kvisitedSelection 2984d7
knegative 607eff
knegativeSelection 607eff
kneutral c794ff
kneutralSelection c794ff
kpositive 00b8de
kpositiveSelection 00b8df
text 191c1e text 191c1e
subtext1 41484e subtext1 41484e
subtext0 6e757c subtext0 6e757c
@@ -82,6 +82,16 @@ sky ccdbff
sapphire b1c6ff sapphire b1c6ff
blue aab9ff blue aab9ff
lavender c2c9ff lavender c2c9ff
klink 6a84d1
klinkSelection 6a84d1
kvisited 7775dc
kvisitedSelection 7775dc
knegative 946dff
knegativeSelection 946dff
kneutral c794ff
kneutralSelection c794ff
kpositive 5daeff
kpositiveSelection 5eaeff
text e4e1e7 text e4e1e7
subtext1 c6c5d1 subtext1 c6c5d1
subtext0 90909a subtext0 90909a
@@ -82,6 +82,16 @@ sky d0daff
sapphire b7c5ff sapphire b7c5ff
blue b0b8ff blue b0b8ff
lavender c7c8ff lavender c7c8ff
klink 7382d2
klinkSelection 7382d2
kvisited 8172da
kvisitedSelection 8172da
knegative a167ff
knegativeSelection a167ff
kneutral ca92ff
kneutralSelection c992ff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7 text e5e1e7
subtext1 c8c5d1 subtext1 c8c5d1
subtext0 918f9a subtext0 918f9a
@@ -0,0 +1,100 @@
primary_paletteKeyColor 33653E
secondary_paletteKeyColor 1B4E2A
tertiary_paletteKeyColor 376942
neutral_paletteKeyColor 1E1E26
neutral_variant_paletteKeyColor 23252D
background 23262D
onBackground 02200B
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
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,100 @@
primary_paletteKeyColor 33653E
secondary_paletteKeyColor 1B4E2A
tertiary_paletteKeyColor 376942
neutral_paletteKeyColor 1E1E26
neutral_variant_paletteKeyColor 23252D
background 23262D
onBackground 02200B
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
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
@@ -82,6 +82,16 @@ sky 97e7fb
sapphire 77d4ef sapphire 77d4ef
blue 65c9ee blue 65c9ee
lavender 90d6f7 lavender 90d6f7
klink 0093b4
klinkSelection 0093b3
kvisited 0089bf
kvisitedSelection 0089be
knegative 607eff
knegativeSelection 607eff
kneutral 34c359
kneutralSelection 34c359
kpositive 00bbc7
kpositiveSelection 00bbc7
text e0e3e4 text e0e3e4
subtext1 bec8cc subtext1 bec8cc
subtext0 889296 subtext0 889296
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 5d7c2e sapphire 5d7c2e
blue 00664e blue 00664e
lavender 00816c lavender 00816c
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative a78300
knegativeSelection a78300
kneutral c7a900
kneutralSelection c7a900
kpositive a0b31d
kpositiveSelection a1b31c
text 1c1c16 text 1c1c16
subtext1 494739 subtext1 494739
subtext0 777565 subtext0 777565
@@ -82,6 +82,16 @@ sky 94e8f6
sapphire 74d5e9 sapphire 74d5e9
blue 5fcae8 blue 5fcae8
lavender 8cd7f3 lavender 8cd7f3
klink 0094ac
klinkSelection 0094ab
kvisited 008bb6
kvisitedSelection 008bb5
knegative 607eff
knegativeSelection 607eff
kneutral 34c359
kneutralSelection 34c359
kpositive 00bcbf
kpositiveSelection 00bcbd
text e0e3e4 text e0e3e4
subtext1 bec8ca subtext1 bec8ca
subtext0 889394 subtext0 889394
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 657b26 sapphire 657b26
blue 00664e blue 00664e
lavender 00816c lavender 00816c
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative ae8000
knegativeSelection ae8000
kneutral d1a500
kneutralSelection d0a500
kpositive adaf00
kpositiveSelection adaf00
text 1d1c15 text 1d1c15
subtext1 4a4738 subtext1 4a4738
subtext0 797564 subtext0 797564
@@ -82,6 +82,16 @@ sky e1df87
sapphire b3d27e sapphire b3d27e
blue ffa2bd blue ffa2bd
lavender ffbcbb lavender ffbcbb
klink bf6ba0
klinkSelection bf6ba0
kvisited cc6232
kvisitedSelection cc6232
knegative d66a00
knegativeSelection d66900
kneutral ff8d00
kneutralSelection ff8d06
kpositive de9d00
kpositiveSelection df9d00
text ece0d9 text ece0d9
subtext1 d6c3b5 subtext1 d6c3b5
subtext0 9f8e81 subtext0 9f8e81
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 6a7a22 sapphire 6a7a22
blue 00664e blue 00664e
lavender c2484e lavender c2484e
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative b27f00
knegativeSelection b27f00
kneutral d5a300
kneutralSelection d5a300
kpositive b3ae00
kpositiveSelection b3ae00
text 1d1b15 text 1d1b15
subtext1 4b4738 subtext1 4b4738
subtext0 7a7464 subtext0 7a7464
@@ -82,6 +82,16 @@ sky c4ddff
sapphire a4caff sapphire a4caff
blue 9abdff blue 9abdff
lavender b7ccff lavender b7ccff
klink 5689ce
klinkSelection 5689ce
kvisited 5f7bdd
kvisitedSelection 5f7bdd
knegative 7877ff
knegativeSelection 7878ff
kneutral c794ff
kneutralSelection c794ff
kpositive 13b3ff
kpositiveSelection 0db3ff
text e3e2e7 text e3e2e7
subtext1 c4c6d0 subtext1 c4c6d0
subtext0 8e909a subtext0 8e909a
@@ -82,6 +82,16 @@ sky c2deff
sapphire a1caff sapphire a1caff
blue 97beff blue 97beff
lavender b5cdff lavender b5cdff
klink 5389ce
klinkSelection 5489ce
kvisited 5b7cdd
kvisitedSelection 5c7bdd
knegative 7479ff
knegativeSelection 7578ff
kneutral c794ff
kneutralSelection c794ff
kpositive 00b4fd
kpositiveSelection 00b4fe
text e3e2e7 text e3e2e7
subtext1 c3c6d0 subtext1 c3c6d0
subtext0 8d919a subtext0 8d919a
@@ -82,6 +82,16 @@ sky 4b882e
sapphire 6d791e sapphire 6d791e
blue 00664e blue 00664e
lavender c2484e lavender c2484e
klink 559652
klinkSelection 559652
kvisited c06b00
kvisitedSelection c06b00
knegative b47d00
knegativeSelection b57d00
kneutral d8a200
kneutralSelection d9a200
kpositive b7ac00
kpositiveSelection b8ac00
text 1e1b15 text 1e1b15
subtext1 4c4638 subtext1 4c4638
subtext0 7b7464 subtext0 7b7464
@@ -82,6 +82,16 @@ sky d3d9ff
sapphire bdc3ff sapphire bdc3ff
blue b7b6ff blue b7b6ff
lavender ccc6ff lavender ccc6ff
klink 7b80d1
klinkSelection 7b80d1
kvisited 8a6fd7
kvisitedSelection 8a6fd7
knegative ac62fa
knegativeSelection ac62fa
kneutral d48dff
kneutralSelection d48eff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7 text e5e1e7
subtext1 c9c4d0 subtext1 c9c4d0
subtext0 938f9a subtext0 938f9a
@@ -82,6 +82,16 @@ sky d2d9ff
sapphire bbc4ff sapphire bbc4ff
blue b5b6ff blue b5b6ff
lavender cbc7ff lavender cbc7ff
klink 7880d1
klinkSelection 7881d1
kvisited 8770d8
kvisitedSelection 8770d8
knegative a964fd
knegativeSelection a864fd
kneutral d08fff
kneutralSelection d090ff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7 text e5e1e7
subtext1 c9c5d0 subtext1 c9c5d0
subtext0 928f9a subtext0 928f9a
@@ -82,6 +82,16 @@ sky cedaff
sapphire b5c5ff sapphire b5c5ff
blue aeb8ff blue aeb8ff
lavender c6c8ff lavender c6c8ff
klink 7083d2
klinkSelection 6f83d2
kvisited 7e73db
kvisitedSelection 7d73db
knegative 9d69ff
knegativeSelection 9b6aff
kneutral c794ff
kneutralSelection c794ff
kpositive 60adff
kpositiveSelection 60adff
text e5e1e7 text e5e1e7
subtext1 c7c5d1 subtext1 c7c5d1
subtext0 918f9a subtext0 918f9a
+57
View File
@@ -0,0 +1,57 @@
# Cava Audio Visualizer Configuration Template
# Optimized for smooth and responsive visualization
[general]
# Number of bars (20-200) - fewer bars = better performance
bars = 64
# Framerate (1-144) - higher = smoother but more CPU intensive
framerate = 60
[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
+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 }}
@@ -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 }}
+2 -2
View File
@@ -1,6 +1,6 @@
[Appearance] [Appearance]
color_scheme_path={{ $config }}/colors/caelestia.conf color_scheme_path={{ $config }}/colors/caelestia.colors
custom_palette=true custom_palette=true
icon_theme=Papirus-{{ $mode }} icon_theme=Papirus-{{ $mode }}
standard_dialogs=default standard_dialogs=default
style=Fusion style=Darkly
+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 }}
+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 }}
+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 }}'
+24 -14
View File
@@ -1,6 +1,6 @@
import argparse 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.paths import wallpapers_dir
from caelestia.utils.scheme import get_scheme_names, scheme_variants from caelestia.utils.scheme import get_scheme_names, scheme_variants
from caelestia.utils.wallpaper import get_wallpaper from caelestia.utils.wallpaper import get_wallpaper
@@ -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("-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("-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("-l", "--log", action="store_true", help="print the shell log")
shell_parser.add_argument( shell_parser.add_argument("-k", "--kill", action="store_true", help="kill the shell")
"--log-rules", shell_parser.add_argument("--log-rules", metavar="RULES", help="log rules to apply")
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",
)
# Create parser for toggle opts # Create parser for toggle opts
toggle_parser = command_parser.add_parser("toggle", help="toggle a special workspace") toggle_parser = command_parser.add_parser("toggle", help="toggle a special workspace")
toggle_parser.set_defaults(cls=toggle.Command) toggle_parser.set_defaults(cls=toggle.Command)
toggle_parser.add_argument( toggle_parser.add_argument("workspace", help="the workspace to toggle")
"workspace", choices=["communication", "music", "sysmon", "specialws", "todo"], help="the workspace to toggle"
)
# Create parser for scheme opts # Create parser for scheme opts
scheme_parser = command_parser.add_parser("scheme", help="manage the colour scheme") scheme_parser = command_parser.add_parser("scheme", help="manage the colour scheme")
@@ -76,6 +70,7 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
record_parser.set_defaults(cls=record.Command) record_parser.set_defaults(cls=record.Command)
record_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="record a region") 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("-s", "--sound", action="store_true", help="record audio")
record_parser.add_argument("-p", "--pause", action="store_true", help="pause/resume the recording")
# Create parser for clipboard opts # Create parser for clipboard opts
clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history") clipboard_parser = command_parser.add_parser("clipboard", help="open clipboard history")
@@ -112,9 +107,24 @@ def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
help="do not automatically change the scheme mode based on wallpaper colour", help="do not automatically change the scheme mode based on wallpaper colour",
) )
# Create parser for pip opts # Create parser for resizer opts
pip_parser = command_parser.add_parser("pip", help="picture in picture utilities") resizer_parser = command_parser.add_parser("resizer", help="window resizer daemon")
pip_parser.set_defaults(cls=pip.Command) resizer_parser.set_defaults(cls=resizer.Command)
pip_parser.add_argument("-d", "--daemon", action="store_true", help="start the daemon") 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() 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)
+61 -42
View File
@@ -1,11 +1,16 @@
import json
import re
import shutil
import subprocess import subprocess
import time import time
from argparse import Namespace from argparse import Namespace
from datetime import datetime from datetime import datetime
from caelestia.utils.notify import notify from caelestia.utils.notify import close_notification, notify
from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir from caelestia.utils.paths import recording_notif_path, recording_path, recordings_dir
RECORDER = "gpu-screen-recorder"
class Command: class Command:
args: Namespace args: Namespace
@@ -14,69 +19,83 @@ class Command:
self.args = args self.args = args
def run(self) -> None: def run(self) -> None:
proc = subprocess.run(["pidof", "wl-screenrec"]) if self.args.pause:
if proc.returncode == 0: subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL)
elif self.proc_running():
self.stop() self.stop()
else: else:
self.start() self.start()
def start(self) -> None: def proc_running(self) -> bool:
args = [] 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 = json.loads(subprocess.check_output(["hyprctl", "monitors", "-j"]))
if self.args.region: if self.args.region:
if self.args.region == "slurp": 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: else:
region = self.args.region region = self.args.region.strip()
args += ["-g", 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: if self.args.sound:
sources = subprocess.check_output(["pactl", "list", "short", "sources"], text=True).splitlines() args += ["-a", "default_output"]
for source in sources:
if "RUNNING" in source:
args += ["--audio", "--audio-device", source.split()[1]]
break
else:
raise ValueError("No audio source found")
recording_path.parent.mkdir(parents=True, exist_ok=True) recording_path.parent.mkdir(parents=True, exist_ok=True)
proc = subprocess.Popen( proc = subprocess.Popen([RECORDER, *args, "-o", str(recording_path)], start_new_session=True)
["wl-screenrec", *args, "--codec", "hevc", "-f", recording_path],
stderr=subprocess.PIPE,
text=True,
start_new_session=True,
)
# Send notif if proc hasn't ended after a small delay notif = notify("-p", "Recording started", "Recording...")
time.sleep(0.1) recording_notif_path.write_text(notif)
if proc.poll() is None:
notif = notify("-p", "Recording started", "Recording...") try:
recording_notif_path.write_text(notif) if proc.wait(1) != 0:
else: close_notification(notif)
notify("Recording failed", f"Recording failed to start: {proc.communicate()[1]}") 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: 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 # Move to recordings folder
new_path = recordings_dir / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4" new_path = recordings_dir / f"recording_{datetime.now().strftime('%Y%m%d_%H-%M-%S')}.mp4"
recordings_dir.mkdir(exist_ok=True, parents=True) recordings_dir.mkdir(exist_ok=True, parents=True)
recording_path.rename(new_path) shutil.move(recording_path, new_path)
# Close start notification # Close start notification
try: try:
notif = recording_notif_path.read_text() close_notification(recording_notif_path.read_text())
subprocess.run(
[
"gdbus",
"call",
"--session",
"--dest=org.freedesktop.Notifications",
"--object-path=/org/freedesktop/Notifications",
"--method=org.freedesktop.Notifications.CloseNotification",
notif,
]
)
except IOError: except IOError:
pass pass
+464
View File
@@ -0,0 +1,464 @@
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("Sign in - Google Accounts", "titleContains", "35%", "65%", ["float", "center"]),
WindowRule("oauth", "titleContains", "30%", "60%", ["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}")
+10 -2
View File
@@ -17,12 +17,17 @@ class Command:
elif self.args.log: elif self.args.log:
# Print the log # Print the log
self.print_log() self.print_log()
elif self.args.kill:
# Kill the shell
self.shell("kill")
elif self.args.message: elif self.args.message:
# Send a message # Send a message
self.message(*self.args.message) self.message(*self.args.message)
else: else:
# Start the shell # 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.append("--log-rules", self.args.log_rules)
if self.args.daemon: if self.args.daemon:
args.append("-d") args.append("-d")
subprocess.run(args) subprocess.run(args)
@@ -42,7 +47,10 @@ class Command:
print(self.shell("ipc", "show"), end="") print(self.shell("ipc", "show"), end="")
def print_log(self) -> None: 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 # FIXME: remove when logging rules are added/warning is removed
for line in log.splitlines(): for line in log.splitlines():
if self.filter_log(line): if self.filter_log(line):
+130 -45
View File
@@ -1,18 +1,124 @@
import subprocess import json
import shlex
import shutil
from argparse import Namespace from argparse import Namespace
from collections import ChainMap
from caelestia.utils import hypr 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: class Command:
args: Namespace args: Namespace
cfg: dict[str, dict[str, dict[str, any]]] | DeepChainMap
clients: list[dict[str, any]] = None clients: list[dict[str, any]] = None
def __init__(self, args: Namespace) -> None: def __init__(self, args: Namespace) -> None:
self.args = args 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: def run(self) -> None:
getattr(self, self.args.workspace)() if self.args.workspace == "specialws":
self.specialws()
return
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]]: def get_clients(self) -> list[dict[str, any]]:
if self.clients is None: if self.clients is None:
@@ -22,54 +128,33 @@ class Command:
def move_client(self, selector: callable, workspace: str) -> None: def move_client(self, selector: callable, workspace: str) -> None:
for client in self.get_clients(): 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']}") hypr.dispatch("movetoworkspacesilent", f"special:{workspace},address:{client['address']}")
def spawn_client(self, selector: callable, spawn: list[str]) -> bool: def spawn_client(self, selector: callable, spawn: list[str]) -> bool:
exists = any(selector(client) for client in self.get_clients()) if (spawn[0].endswith(".desktop") or shutil.which(spawn[0])) and not any(
selector(client) for client in self.get_clients()
):
hypr.dispatch("exec", f"[workspace special:{self.args.workspace}] app2unit -- {shlex.join(spawn)}")
return True
return False
if not exists: def handle_client_config(self, client: dict[str, any]) -> bool:
subprocess.Popen(["app2unit", "--", *spawn], start_new_session=True) 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: return spawned
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")
def specialws(self) -> None: def specialws(self) -> None:
workspaces = hypr.message("workspaces") special = next(m for m in hypr.message("monitors") if m["focused"])["specialWorkspace"]["name"]
on_special_ws = any(ws["name"] == "special:special" for ws in workspaces) hypr.dispatch("togglespecialworkspace", special[8:] or "special")
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)
+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()}
+6
View File
@@ -27,3 +27,9 @@ def message(msg: str, json: bool = True) -> str | dict[str, any]:
def dispatch(dispatcher: str, *args: list[any]) -> bool: def dispatch(dispatcher: str, *args: list[any]) -> bool:
return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok" return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok"
def batch(*msgs: list[str], json: bool = False) -> str | dict[str, any]:
if json:
msgs = (f"j/{m.strip()}" for m in msgs)
return message(f"[[BATCH]]{';'.join(msgs)}", json=False)
+20
View File
@@ -0,0 +1,20 @@
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
+16
View File
@@ -92,6 +92,14 @@ dark_catppuccin = [
hex_to_hct("b4befe"), 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 = [ colour_names = [
"rosewater", "rosewater",
"flamingo", "flamingo",
@@ -185,6 +193,14 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
else: 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 light else 0.05))
# KColours
for colour in kcolours:
colours[colour["name"]] = harmonize(colour["hct"], colours["primary"], 0.1)
colours[f"{colour['name']}Selection"] = harmonize(colour["hct"], colours["onPrimaryFixedVariant"], 0.1)
if scheme.variant == "monochrome":
colours[colour["name"]] = grayscale(colours[colour["name"]], light)
colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], light)
if scheme.variant == "neutral": if scheme.variant == "neutral":
for name, hct in colours.items(): for name, hct in colours.items():
colours[name].chroma -= 15 colours[name].chroma -= 15
+15
View File
@@ -3,3 +3,18 @@ import subprocess
def notify(*args: list[str]) -> str: def notify(*args: list[str]) -> str:
return subprocess.check_output(["notify-send", "-a", "caelestia-cli", *args], text=True).strip() 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,
)
+8 -3
View File
@@ -9,29 +9,34 @@ config_dir = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
data_dir = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share")) data_dir = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share"))
state_dir = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state")) state_dir = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state"))
cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache")) cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache"))
pictures_dir = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures"))
videos_dir = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos"))
c_config_dir = config_dir / "caelestia" c_config_dir = config_dir / "caelestia"
c_data_dir = data_dir / "caelestia" c_data_dir = data_dir / "caelestia"
c_state_dir = state_dir / "caelestia" c_state_dir = state_dir / "caelestia"
c_cache_dir = cache_dir / "caelestia" c_cache_dir = cache_dir / "caelestia"
user_config_path = c_config_dir / "cli.json"
cli_data_dir = Path(__file__).parent.parent / "data" cli_data_dir = Path(__file__).parent.parent / "data"
templates_dir = cli_data_dir / "templates" templates_dir = cli_data_dir / "templates"
user_templates_dir = c_config_dir / "templates"
theme_dir = c_state_dir / "theme"
scheme_path = c_state_dir / "scheme.json" scheme_path = c_state_dir / "scheme.json"
scheme_data_dir = cli_data_dir / "schemes" scheme_data_dir = cli_data_dir / "schemes"
scheme_cache_dir = c_cache_dir / "schemes" scheme_cache_dir = c_cache_dir / "schemes"
wallpapers_dir = Path.home() / "Pictures/Wallpapers" wallpapers_dir = os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers")
wallpaper_path_path = c_state_dir / "wallpaper/path.txt" wallpaper_path_path = c_state_dir / "wallpaper/path.txt"
wallpaper_link_path = c_state_dir / "wallpaper/current" wallpaper_link_path = c_state_dir / "wallpaper/current"
wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg" wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg"
wallpapers_cache_dir = c_cache_dir / "wallpapers" wallpapers_cache_dir = c_cache_dir / "wallpapers"
screenshots_dir = Path.home() / "Pictures/Screenshots" screenshots_dir = os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots")
screenshots_cache_dir = c_cache_dir / "screenshots" screenshots_cache_dir = c_cache_dir / "screenshots"
recordings_dir = Path.home() / "Videos/Recordings" recordings_dir = os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings")
recording_path = c_state_dir / "record/recording.mp4" recording_path = c_state_dir / "record/recording.mp4"
recording_notif_path = c_state_dir / "record/notifid.txt" recording_notif_path = c_state_dir / "record/notifid.txt"
+134 -14
View File
@@ -1,7 +1,19 @@
import json
import re
import subprocess import subprocess
from pathlib import Path 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,
)
def gen_conf(colours: dict[str, str]) -> str: def gen_conf(colours: dict[str, str]) -> str:
@@ -25,6 +37,25 @@ def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) ->
return template return template
def gen_replace_dynamic(colours: dict[str, str], template: Path) -> 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
field = r"\{\{((?:(?!\{\{|\}\}).)*)\}\}"
colours_dyn = get_dynamic_colours(colours)
template_content = template.read_text()
template_filled = re.sub(field, fill_colour, template_content)
return template_filled
def c2s(c: str, *i: list[int]) -> str: def c2s(c: str, *i: list[int]) -> str:
"""Hex to ANSI sequence (e.g. ffffff, 11 -> \x1b]11;rgb:ff/ff/ff\x1b\\)""" """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\\" return f"\x1b]{';'.join(map(str, i))};rgb:{c[0:2]}/{c[2:4]}/{c[4:6]}\x1b\\"
@@ -73,6 +104,7 @@ def write_file(path: Path, content: str) -> None:
path.write_text(content) path.write_text(content)
@log_exception
def apply_terms(sequences: str) -> None: def apply_terms(sequences: str) -> None:
state = c_state_dir / "sequences.txt" state = c_state_dir / "sequences.txt"
state.parent.mkdir(parents=True, exist_ok=True) state.parent.mkdir(parents=True, exist_ok=True)
@@ -81,14 +113,19 @@ def apply_terms(sequences: str) -> None:
pts_path = Path("/dev/pts") pts_path = Path("/dev/pts")
for pt in pts_path.iterdir(): for pt in pts_path.iterdir():
if pt.name.isdigit(): if pt.name.isdigit():
with pt.open("a") as f: try:
f.write(sequences) with pt.open("a") as f:
f.write(sequences)
except PermissionError:
pass
@log_exception
def apply_hypr(conf: str) -> None: def apply_hypr(conf: str) -> None:
write_file(config_dir / "hypr/scheme/current.conf", conf) write_file(config_dir / "hypr/scheme/current.conf", conf)
@log_exception
def apply_discord(scss: str) -> None: def apply_discord(scss: str) -> None:
import tempfile import tempfile
@@ -100,22 +137,39 @@ def apply_discord(scss: str) -> None:
write_file(config_dir / client / "themes/caelestia.theme.css", conf) write_file(config_dir / client / "themes/caelestia.theme.css", conf)
@log_exception
def apply_spicetify(colours: dict[str, str], mode: str) -> None: def apply_spicetify(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / f"spicetify-{mode}.ini") template = gen_replace(colours, templates_dir / f"spicetify-{mode}.ini")
write_file(config_dir / "spicetify/Themes/caelestia/color.ini", template) write_file(config_dir / "spicetify/Themes/caelestia/color.ini", template)
@log_exception
def apply_fuzzel(colours: dict[str, str]) -> None: def apply_fuzzel(colours: dict[str, str]) -> None:
template = gen_replace(colours, templates_dir / "fuzzel.ini") template = gen_replace(colours, templates_dir / "fuzzel.ini")
write_file(config_dir / "fuzzel/fuzzel.ini", template) write_file(config_dir / "fuzzel/fuzzel.ini", template)
@log_exception
def apply_btop(colours: dict[str, str]) -> None: def apply_btop(colours: dict[str, str]) -> None:
template = gen_replace(colours, templates_dir / "btop.theme", hash=True) template = gen_replace(colours, templates_dir / "btop.theme", hash=True)
write_file(config_dir / "btop/themes/caelestia.theme", template) write_file(config_dir / "btop/themes/caelestia.theme", template)
subprocess.run(["killall", "-USR2", "btop"], stderr=subprocess.DEVNULL) subprocess.run(["killall", "-USR2", "btop"], stderr=subprocess.DEVNULL)
@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)
@log_exception
def apply_gtk(colours: dict[str, str], mode: str) -> None: def apply_gtk(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / "gtk.css", hash=True) 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-3.0/gtk.css", template)
@@ -126,25 +180,91 @@ def apply_gtk(colours: dict[str, str], mode: str) -> None:
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'Papirus-{mode.capitalize()}'"]) subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'Papirus-{mode.capitalize()}'"])
@log_exception
def apply_qt(colours: dict[str, str], mode: str) -> None: def apply_qt(colours: dict[str, str], mode: str) -> None:
template = gen_replace(colours, templates_dir / "qtcolors.conf", hash=True) template = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True)
write_file(config_dir / "qt5ct/colors/caelestia.conf", template) write_file(config_dir / "qt5ct/colors/caelestia.colors", template)
write_file(config_dir / "qt6ct/colors/caelestia.conf", template) write_file(config_dir / "qt6ct/colors/caelestia.colors", template)
qtct = (templates_dir / "qtct.conf").read_text() qtct = (templates_dir / "qtct.conf").read_text()
qtct = qtct.replace("{{ $mode }}", mode.capitalize()) qtct = qtct.replace("{{ $mode }}", mode.capitalize())
for ver in 5, 6: for ver in 5, 6:
conf = qtct.replace("{{ $config }}", str(config_dir / f"qt{ver}ct")) conf = qtct.replace("{{ $config }}", str(config_dir / f"qt{ver}ct"))
if ver == 5:
conf += """
[Fonts]
fixed="Monospace,12,-1,5,50,0,0,0,0,0"
general="Sans Serif,12,-1,5,50,0,0,0,0,0"
"""
else:
conf += """
[Fonts]
fixed="Monospace,12,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
general="Sans Serif,12,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
"""
write_file(config_dir / f"qt{ver}ct/qt{ver}ct.conf", conf) 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_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]) -> 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)
write_file(theme_dir / file.name, content)
def apply_colours(colours: dict[str, str], mode: str) -> None: def apply_colours(colours: dict[str, str], mode: str) -> None:
apply_terms(gen_sequences(colours)) try:
apply_hypr(gen_conf(colours)) cfg = json.loads(user_config_path.read_text())["theme"]
apply_discord(gen_scss(colours)) except (FileNotFoundError, json.JSONDecodeError, KeyError):
apply_spicetify(colours, mode) cfg = {}
apply_fuzzel(colours)
apply_btop(colours) def check(key: str) -> bool:
apply_gtk(colours, mode) return cfg[key] if key in cfg else True
apply_qt(colours, mode)
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("enableFuzzel"):
apply_fuzzel(colours)
if check("enableBtop"):
apply_btop(colours)
if check("enableNvtop"):
apply_nvtop(colours)
if check("enableHtop"):
apply_htop(colours)
if check("enableGtk"):
apply_gtk(colours, mode)
if check("enableQt"):
apply_qt(colours, mode)
if check("enableWarp"):
apply_warp(colours, mode)
if check("enableCava"):
apply_cava(colours)
apply_user_templates(colours)
+38 -18
View File
@@ -1,30 +1,50 @@
import shutil
import subprocess import subprocess
from caelestia.utils.paths import config_dir from caelestia.utils.paths import config_dir
def print_version() -> None: def print_version() -> None:
print("Packages:") if shutil.which("pacman"):
pkgs = ["caelestia-shell-git", "caelestia-cli-git", "caelestia-meta"] print("Packages:")
versions = subprocess.run( pkgs = ["caelestia-shell", "caelestia-cli", "caelestia-meta"]
["pacman", "-Q", *pkgs], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True versions = subprocess.run(
).stdout ["pacman", "-Q", *pkgs], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
).stdout
for pkg in pkgs: for pkg in pkgs:
if pkg not in versions: if pkg not in versions:
print(f" {pkg} not installed") print(f" {pkg} not installed")
print("\n".join(f" {pkg}" for pkg in versions.splitlines())) print("\n".join(f" {pkg}" for pkg in versions.splitlines()))
else:
print("Packages: not on Arch")
caelestia_dir = (config_dir / "hypr").resolve().parent print()
print("\nCaelestia:") try:
caelestia_ver = subprocess.check_output( caelestia_dir = (config_dir / "hypr").resolve().parent
["git", "--git-dir", caelestia_dir / ".git", "rev-list", "--format=%B", "--max-count=1", "HEAD"], text=True 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("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()
print(" ", subprocess.check_output(["qs", "--version"], text=True).strip()) try:
shell_ver = subprocess.check_output(["/usr/lib/caelestia/version", "-s"], text=True).strip()
print("Shell:")
print(" ", shell_ver)
except subprocess.CalledProcessError:
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" local_shell_dir = config_dir / "quickshell/caelestia"
if local_shell_dir.exists(): if local_shell_dir.exists():
+15 -1
View File
@@ -160,4 +160,18 @@ def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
def set_random(args: Namespace) -> None: 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)