mirror of
https://github.com/caelestia-dots/cli.git
synced 2026-06-20 07:50:00 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a0bd16dd5 | |||
| f737ed6cc4 | |||
| dca14f0fa9 | |||
| 9ad953214f | |||
| 92d52e5602 | |||
| 525cab74fe | |||
| e71b0fe41f | |||
| 6cf5d2bd02 | |||
| e5f1231749 |
@@ -1,20 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# 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 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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."
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -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: ./bin/caelestia emoji -f
|
run: ./run.sh emoji -f
|
||||||
|
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
id: check
|
id: check
|
||||||
run: echo modified=$(git diff --exit-code src/caelestia/data/emojis.txt &>/dev/null && echo 'false' || echo 'true') >> $GITHUB_OUTPUT
|
run: echo modified=$(test -n "$(git status --porcelain)" && echo 'true' || echo 'false') >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Commit and push changes
|
- name: Commit and push changes
|
||||||
if: steps.check.outputs.modified == 'true'
|
if: steps.check.outputs.modified == 'true'
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -4,67 +4,35 @@ The main control script for the Caelestia dotfiles.
|
|||||||
|
|
||||||
<details><summary id="dependencies">External dependencies</summary>
|
<details><summary id="dependencies">External dependencies</summary>
|
||||||
|
|
||||||
- [`libnotfy`](https://gitlab.gnome.org/GNOME/libnotify) - sending notifications
|
- [`libnotfy`](https://gitlab.gnome.org/GNOME/libnotify) - sending notifications
|
||||||
- [`swappy`](https://github.com/jtheoof/swappy) - screenshot editor
|
- [`swappy`](https://github.com/jtheoof/swappy) - screenshot editor
|
||||||
- [`grim`](https://gitlab.freedesktop.org/emersion/grim) - taking screenshots
|
- [`grim`](https://gitlab.freedesktop.org/emersion/grim) - taking screenshots
|
||||||
- [`dart-sass`](https://github.com/sass/dart-sass) - discord theming
|
- [`dart-sass`](https://github.com/sass/dart-sass) - discord theming
|
||||||
- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard
|
- [`app2unit`](https://github.com/Vladimir-csp/app2unit) - launching apps
|
||||||
- [`slurp`](https://github.com/emersion/slurp) - selecting an area
|
- [`wl-clipboard`](https://github.com/bugaevc/wl-clipboard) - copying to clipboard
|
||||||
- [`gpu-screen-recorder`](https://git.dec05eba.com/gpu-screen-recorder/about) - screen recording
|
- [`slurp`](https://github.com/emersion/slurp) - selecting an area
|
||||||
- `glib2` - closing notifications
|
- [`wl-screenrec`](https://github.com/russelltg/wl-screenrec) - screen recording
|
||||||
- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history
|
- `glib2` - closing notifications
|
||||||
- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
|
- `libpulse` - getting audio device
|
||||||
|
- [`cliphist`](https://github.com/sentriz/cliphist) - clipboard history
|
||||||
|
- [`fuzzel`](https://codeberg.org/dnkl/fuzzel) - clipboard history/emoji picker
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Arch linux
|
### Package manager (recommended)
|
||||||
|
|
||||||
The CLI is available from the AUR as `caelestia-cli`. You can install it with an AUR helper
|
The cli is available from the AUR as `caelestia-cli-git`. To install it you can use
|
||||||
like [`yay`](https://github.com/Jguer/yay) or manually downloading the PKGBUILD and running `makepkg -si`.
|
an AUR helper like [`yay`](https://github.com/Jguer/yay), or manually download the
|
||||||
|
PKGBUILD and run `makepkg -si`.
|
||||||
|
|
||||||
A package following the latest commit also exists as `caelestia-cli-git`. This is bleeding edge
|
e.g. using yay
|
||||||
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
|
||||||
nix run github:caelestia-dots/cli
|
yay -S caelestia-cli-git
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
@@ -76,7 +44,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 wl-clipboard slurp gpu-screen-recorder glib2 cliphist fuzzel python-build python-installer python-hatch python-hatch-vcs
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
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`
|
||||||
@@ -92,177 +60,30 @@ sudo python -m installer dist/*.whl
|
|||||||
sudo cp completions/caelestia.fish /usr/share/fish/vendor_completions.d/caelestia.fish
|
sudo cp completions/caelestia.fish /usr/share/fish/vendor_completions.d/caelestia.fish
|
||||||
```
|
```
|
||||||
|
|
||||||
### Additional steps
|
|
||||||
|
|
||||||
#### Auto folder colour theming
|
|
||||||
|
|
||||||
For automatic Papirus folder icon colour syncing, you must have [`papirus-folders`](https://github.com/PapirusDevelopmentTeam/papirus-folders)
|
|
||||||
installed, and `papirus-folders` must to be able to run with `sudo` without a password prompt.
|
|
||||||
|
|
||||||
You can allow this by creating a sudoers file:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
echo "$USER ALL=(ALL) NOPASSWD: $(which papirus-folders)" | sudo tee /etc/sudoers.d/papirus-folders
|
|
||||||
sudo chmod 440 /etc/sudoers.d/papirus-folders
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Chromium-based browser theming
|
|
||||||
|
|
||||||
For live Chromium-based browser theming, the CLI must be allowed to create certain directories in `/etc`
|
|
||||||
and write to them via `sudo` without a password prompt.
|
|
||||||
|
|
||||||
You can allow this by creating a sudoers file:
|
|
||||||
|
|
||||||
```fish
|
|
||||||
# Fish shell
|
|
||||||
for dir in /etc/chromium/policies/managed /etc/brave/policies/managed /etc/opt/chrome/policies/managed
|
|
||||||
echo "$USER ALL=(ALL) NOPASSWD: $(which mkdir) -p $dir" | sudo tee -a /etc/sudoers.d/caelestia-chromium
|
|
||||||
echo "$USER ALL=(ALL) NOPASSWD: $(which tee) $dir/caelestia.json" | sudo tee -a /etc/sudoers.d/caelestia-chromium
|
|
||||||
end
|
|
||||||
sudo chmod 440 /etc/sudoers.d/caelestia-chromium
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Bash/other shells
|
|
||||||
for dir in /etc/chromium/policies/managed /etc/brave/policies/managed /etc/opt/chrome/policies/managed; do
|
|
||||||
echo "$USER ALL=(ALL) NOPASSWD: $(which mkdir) -p $dir" | sudo tee -a /etc/sudoers.d/caelestia-chromium
|
|
||||||
echo "$USER ALL=(ALL) NOPASSWD: $(which tee) $dir/caelestia.json" | sudo tee -a /etc/sudoers.d/caelestia-chromium
|
|
||||||
done
|
|
||||||
sudo chmod 440 /etc/sudoers.d/caelestia-chromium
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
All subcommands/options can be explored via the help flag.
|
All subcommands/options can be explored via the help flag.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ caelestia -h
|
$ caelestia -h
|
||||||
usage: caelestia [-h] [-v] COMMAND ...
|
usage: caelestia [-h] 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
|
||||||
resizer window resizer daemon
|
pip picture in picture utilities
|
||||||
install install the Caelestia dotfiles
|
|
||||||
update update the Caelestia dotfiles
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### User templates
|
|
||||||
|
|
||||||
Custom user templates can be defined in `~/.config/caelestia/templates/`.
|
|
||||||
|
|
||||||
#### Template syntax
|
|
||||||
|
|
||||||
`{{ <color>.<format> }}`
|
|
||||||
|
|
||||||
- `<color>` is a theme color role derived from the Material You color system (e.g. `primary`, `secondary`, `background`)
|
|
||||||
- `<format>` is the output format: `hex` or `rgb`
|
|
||||||
|
|
||||||
#### Examples
|
|
||||||
|
|
||||||
- `{{ primary.hex }}` outputs `3f4ba2`
|
|
||||||
- `{{ primary.rgb }}` outputs `rgb(193, 132, 207)`
|
|
||||||
|
|
||||||
Output files are written to `~/.local/state/caelestia/theme/`. You can symlink them to your desired locations.
|
|
||||||
|
|
||||||
## Configuring
|
|
||||||
|
|
||||||
All configuration options are in `~/.config/caelestia/cli.json`.
|
|
||||||
|
|
||||||
<details><summary>Example configuration</summary>
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"record": {
|
|
||||||
"extraArgs": []
|
|
||||||
},
|
|
||||||
"wallpaper": {
|
|
||||||
"postHook": "echo $WALLPAPER_PATH $SCHEME_NAME $SCHEME_FLAVOUR $SCHEME_MODE $SCHEME_VARIANT $SCHEME_COLOURS"
|
|
||||||
},
|
|
||||||
"theme": {
|
|
||||||
"enableTerm": true,
|
|
||||||
"enableHypr": true,
|
|
||||||
"enableDiscord": true,
|
|
||||||
"enableSpicetify": true,
|
|
||||||
"enablePandora": true,
|
|
||||||
"enableFuzzel": true,
|
|
||||||
"enableBtop": true,
|
|
||||||
"enableNvtop": true,
|
|
||||||
"enableHtop": true,
|
|
||||||
"enableGtk": true,
|
|
||||||
"enableQt": true,
|
|
||||||
"enableWarp": true,
|
|
||||||
"enableChromium": true,
|
|
||||||
"enableZed": true,
|
|
||||||
"enableCava": true,
|
|
||||||
"iconTheme": "Papirus-Dark",
|
|
||||||
"iconThemeLight": "Papirus-Light",
|
|
||||||
"iconThemeDark": "Papirus-Dark",
|
|
||||||
"postHook": "echo $SCHEME_NAME $SCHEME_FLAVOUR $SCHEME_MODE $SCHEME_VARIANT $SCHEME_COLOURS"
|
|
||||||
},
|
|
||||||
"toggles": {
|
|
||||||
"communication": {
|
|
||||||
"discord": {
|
|
||||||
"enable": true,
|
|
||||||
"match": [{ "class": "discord" }],
|
|
||||||
"command": ["discord"],
|
|
||||||
"move": true
|
|
||||||
},
|
|
||||||
"whatsapp": {
|
|
||||||
"enable": true,
|
|
||||||
"match": [{ "class": "whatsapp" }],
|
|
||||||
"move": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"music": {
|
|
||||||
"spotify": {
|
|
||||||
"enable": true,
|
|
||||||
"match": [{ "class": "Spotify" }, { "initialTitle": "Spotify" }, { "initialTitle": "Spotify Free" }],
|
|
||||||
"command": ["spicetify", "watch", "-s"],
|
|
||||||
"move": true
|
|
||||||
},
|
|
||||||
"feishin": {
|
|
||||||
"enable": true,
|
|
||||||
"match": [{ "class": "feishin" }],
|
|
||||||
"move": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sysmon": {
|
|
||||||
"btop": {
|
|
||||||
"enable": true,
|
|
||||||
"match": [{ "class": "btop", "title": "btop", "workspace": { "name": "special:sysmon" } }],
|
|
||||||
"command": ["foot", "-a", "btop", "-T", "btop", "fish", "-C", "exec btop"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"todo": {
|
|
||||||
"todoist": {
|
|
||||||
"enable": true,
|
|
||||||
"match": [{ "class": "Todoist" }],
|
|
||||||
"command": ["todoist"],
|
|
||||||
"move": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dots": {
|
|
||||||
"url": "https://github.com/caelestia-dots/caelestia.git",
|
|
||||||
"branch": "main"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
export HOME=/tmp/install-test
|
|
||||||
export XDG_CONFIG_HOME=$HOME/.config
|
|
||||||
export XDG_DATA_HOME=$HOME/.local/share
|
|
||||||
export XDG_STATE_HOME=$HOME/.local/state
|
|
||||||
export XDG_CACHE_HOME=$HOME/.cache
|
|
||||||
|
|
||||||
"$@"
|
|
||||||
@@ -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 resizer install update
|
set -l commands shell toggle scheme screenshot record clipboard emoji-picker wallpaper pip
|
||||||
set -l not_seen "not $seen $commands"
|
set -l not_seen "not $seen $commands"
|
||||||
|
|
||||||
# Disable file completions
|
# Disable file completions
|
||||||
@@ -19,9 +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 'resizer' -d 'Window resizer'
|
complete -c caelestia -n $not_seen -a 'pip' -d 'Picture in picture utilities'
|
||||||
complete -c caelestia -n $not_seen -a 'install' -d 'Install the Caelestia dotfiles'
|
|
||||||
complete -c caelestia -n $not_seen -a 'update' -d 'Update the Caelestia dotfiles'
|
|
||||||
|
|
||||||
# Shell
|
# Shell
|
||||||
set -l commands mpris drawers wallpaper notifs
|
set -l commands mpris drawers wallpaper notifs
|
||||||
@@ -60,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 2> /dev/null)
|
set -l commands (caelestia shell drawers list)
|
||||||
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
|
||||||
@@ -73,7 +71,7 @@ complete -c caelestia -n "$seen shell && $seen wallpaper && $seen set" -F
|
|||||||
complete -c caelestia -n "$seen shell && $seen notifs && not $seen clear" -a 'clear' -d 'Clear popup notifications'
|
complete -c caelestia -n "$seen shell && $seen notifs && not $seen clear" -a 'clear' -d 'Clear popup notifications'
|
||||||
|
|
||||||
# Toggles
|
# Toggles
|
||||||
set -l commands communication music specialws sysmon todo
|
set -l commands communication music specialws sysmon todo steam
|
||||||
complete -c caelestia -n "$seen toggle && not $seen drawers && not $seen $commands" -a "$commands" -d 'toggle'
|
complete -c caelestia -n "$seen toggle && not $seen drawers && not $seen $commands" -a "$commands" -d 'toggle'
|
||||||
|
|
||||||
# Scheme
|
# Scheme
|
||||||
@@ -107,7 +105,6 @@ complete -c caelestia -n "$seen screenshot" -s 'f' -l 'freeze' -d 'Freeze while
|
|||||||
# Record
|
# Record
|
||||||
complete -c caelestia -n "$seen record" -s 'r' -l 'region' -d 'Capture region'
|
complete -c caelestia -n "$seen record" -s 'r' -l 'region' -d 'Capture region'
|
||||||
complete -c caelestia -n "$seen record" -s 's' -l 'sound' -d 'Capture sound'
|
complete -c caelestia -n "$seen record" -s 's' -l 'sound' -d 'Capture sound'
|
||||||
complete -c caelestia -n "$seen record" -s 'c' -l 'clipboard' -d 'Copy recording path to clipboard'
|
|
||||||
|
|
||||||
# Clipboard
|
# Clipboard
|
||||||
complete -c caelestia -n "$seen clipboard" -s 'd' -l 'delete' -d 'Delete from cliboard history'
|
complete -c caelestia -n "$seen clipboard" -s 'd' -l 'delete' -d 'Delete from cliboard history'
|
||||||
@@ -124,17 +121,5 @@ 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'
|
||||||
|
|
||||||
# Resizer
|
# Pip
|
||||||
complete -c caelestia -n "$seen resizer" -s 'd' -l 'daemon' -d 'Start in daemon mode'
|
complete -c caelestia -n "$seen pip" -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'
|
|
||||||
|
|
||||||
# Install (component flags come from the manifest, so are not completed statically)
|
|
||||||
complete -c caelestia -n "$seen install" -l 'aur-helper' -d 'The AUR helper to use' -a 'yay paru' -r
|
|
||||||
complete -c caelestia -n "$seen install" -l 'enable-components' -d 'List of components to enable' -r
|
|
||||||
complete -c caelestia -n "$seen install" -l 'disable-components' -d 'List of components to disable' -r
|
|
||||||
complete -c caelestia -n "$seen install" -l 'noconfirm' -d 'Use defaults for all prompts'
|
|
||||||
|
|
||||||
# Update
|
|
||||||
complete -c caelestia -n "$seen update" -l 'aur-helper' -d 'The AUR helper to use' -a 'yay paru' -r
|
|
||||||
complete -c caelestia -n "$seen update" -l 'noconfirm' -d 'Use defaults for all prompts'
|
|
||||||
|
|||||||
+9
-9
@@ -8,17 +8,17 @@
|
|||||||
slurp,
|
slurp,
|
||||||
wl-clipboard,
|
wl-clipboard,
|
||||||
cliphist,
|
cliphist,
|
||||||
xdg-utils,
|
app2unit,
|
||||||
dart-sass,
|
dart-sass,
|
||||||
grim,
|
grim,
|
||||||
fuzzel,
|
fuzzel,
|
||||||
gpu-screen-recorder,
|
wl-screenrec,
|
||||||
dconf,
|
dconf,
|
||||||
killall,
|
killall,
|
||||||
caelestia-shell,
|
caelestia-shell,
|
||||||
withShell ? false,
|
withShell ? false,
|
||||||
discordBin ? "discord",
|
discordBin ? "discord",
|
||||||
qtctStyle ? "Darkly",
|
qtctStyle ? "Fusion",
|
||||||
}:
|
}:
|
||||||
python3.pkgs.buildPythonApplication {
|
python3.pkgs.buildPythonApplication {
|
||||||
pname = "caelestia-cli";
|
pname = "caelestia-cli";
|
||||||
@@ -46,11 +46,11 @@ python3.pkgs.buildPythonApplication {
|
|||||||
slurp
|
slurp
|
||||||
wl-clipboard
|
wl-clipboard
|
||||||
cliphist
|
cliphist
|
||||||
xdg-utils
|
app2unit
|
||||||
dart-sass
|
dart-sass
|
||||||
grim
|
grim
|
||||||
fuzzel
|
fuzzel
|
||||||
gpu-screen-recorder
|
wl-screenrec
|
||||||
dconf
|
dconf
|
||||||
killall
|
killall
|
||||||
]
|
]
|
||||||
@@ -68,11 +68,11 @@ python3.pkgs.buildPythonApplication {
|
|||||||
# Use config bin instead of discord + fix todoist
|
# Use config bin instead of discord + fix todoist
|
||||||
substituteInPlace src/caelestia/subcommands/toggle.py \
|
substituteInPlace src/caelestia/subcommands/toggle.py \
|
||||||
--replace-fail 'discord' ${discordBin} \
|
--replace-fail 'discord' ${discordBin} \
|
||||||
--replace-fail '["todoist"]' '["todoist.desktop"]'
|
--replace-fail 'todoist' 'todoist.desktop'
|
||||||
|
|
||||||
# Use config style instead of darkly
|
# Use config style instead of fusion
|
||||||
substituteInPlace src/caelestia/data/templates/qtengine.json \
|
substituteInPlace src/caelestia/data/templates/qtct.conf \
|
||||||
--replace-fail 'Darkly' '${qtctStyle}'
|
--replace-fail 'Fusion' '${qtctStyle}'
|
||||||
'';
|
'';
|
||||||
|
|
||||||
postInstall = "installShellCompletion completions/caelestia.fish";
|
postInstall = "installShellCompletion completions/caelestia.fish";
|
||||||
|
|||||||
Generated
+60
-29
@@ -1,20 +1,67 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
"caelestia-cli": [],
|
"app2unit": [
|
||||||
"m3shapes": "m3shapes",
|
"app2unit"
|
||||||
|
],
|
||||||
|
"caelestia-cli": "caelestia-cli",
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
],
|
],
|
||||||
"quickshell": "quickshell"
|
"quickshell": "quickshell"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781850732,
|
"lastModified": 1752637099,
|
||||||
"narHash": "sha256-YKAWz4bSguUWwc1GxOHXRFl4fT+t9WnA2VoZGIRdFVc=",
|
"narHash": "sha256-08oPnEGYkuU7Vqa4F7rOi4E9j2Drigm3DxdOA+/mgF4=",
|
||||||
"owner": "caelestia-dots",
|
"owner": "caelestia-dots",
|
||||||
"repo": "shell",
|
"repo": "shell",
|
||||||
"rev": "37e603fbf6f973a09f451553b61ac584d9877cf1",
|
"rev": "19431534c954f763eb095dd131fd0b19ff74837b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -23,30 +70,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"m3shapes": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1781017666,
|
|
||||||
"narHash": "sha256-kfHyzZaPHgqZML48OA+5JwBOsLdQJ2ci/aGPShvUB4Y=",
|
|
||||||
"owner": "soramanew",
|
|
||||||
"repo": "m3shapes",
|
|
||||||
"rev": "bdc327b29f95394a732baf3c9b19658ba23755b6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "soramanew",
|
|
||||||
"repo": "m3shapes",
|
|
||||||
"rev": "bdc327b29f95394a732baf3c9b19658ba23755b6",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781577229,
|
"lastModified": 1752480373,
|
||||||
"narHash": "sha256-lrp67w8AulE9Ks53n27I45ADSzbOCn4H+CNW1Ck8B+8=",
|
"narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "567a49d1913ce81ac6e9582e3553dd90a955875f",
|
"rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -64,11 +94,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781053488,
|
"lastModified": 1752631407,
|
||||||
"narHash": "sha256-P4WEBaKgl8flRckHxXGHzT0potPvB3x8ZFIp9gLEAMY=",
|
"narHash": "sha256-dLDtKxh1VabwLxv5xbjI+oRkDyqWEKGITU+0dEaaW28=",
|
||||||
"ref": "refs/heads/master",
|
"ref": "refs/heads/master",
|
||||||
"rev": "d99d87d5e5ec4e696815348692fdaaf0b6be1b2c",
|
"rev": "4d8055f1cd9924bcace59405894b8879633eb83d",
|
||||||
"revCount": 822,
|
"revCount": 638,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
|
"url": "https://git.outfoxxed.me/outfoxxed/quickshell"
|
||||||
},
|
},
|
||||||
@@ -79,6 +109,7 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"app2unit": "app2unit",
|
||||||
"caelestia-shell": "caelestia-shell",
|
"caelestia-shell": "caelestia-shell",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,15 @@
|
|||||||
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.caelestia-cli.follows = "";
|
inputs.app2unit.follows = "app2unit";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,15 +31,20 @@
|
|||||||
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 {
|
||||||
packages = [self.packages.${pkgs.system}.with-shell];
|
inputsFrom = [self.packages.${pkgs.system}.caelestia-cli];
|
||||||
|
packages = [
|
||||||
|
(pkgs.writeShellScriptBin "caelestia" ''
|
||||||
|
cd src && python -m caelestia "$@"
|
||||||
|
'')
|
||||||
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,13 +16,3 @@ 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
|
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
from caelestia.parser import parse_args
|
from caelestia.parser import parse_args
|
||||||
from caelestia.utils.io import log
|
|
||||||
from caelestia.utils.version import print_version
|
from caelestia.utils.version import print_version
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
try:
|
parser, args = parse_args()
|
||||||
parser, args = parse_args()
|
if args.version:
|
||||||
if args.version:
|
print_version()
|
||||||
print_version()
|
elif "cls" in args:
|
||||||
elif "cls" in args:
|
args.cls(args).run()
|
||||||
args.cls(args).run()
|
else:
|
||||||
else:
|
parser.print_help()
|
||||||
parser.print_help()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
log("Exiting...")
|
|
||||||
|
|||||||
@@ -132,7 +132,6 @@
|
|||||||
😯 hushed face epic face hushed omg stunned surprised whoa woah
|
😯 hushed face epic face hushed omg stunned surprised whoa woah
|
||||||
😲 :o :O astonished face astonished cost face no omg shocked totally way
|
😲 :o :O astonished face astonished cost face no omg shocked totally way
|
||||||
😳 :$ flushed face amazed awkward crazy dazed dead disbelief embarrassed face flushed geez heat hot impressed jeez what wow
|
😳 :$ flushed face amazed awkward crazy dazed dead disbelief embarrassed face flushed geez heat hot impressed jeez what wow
|
||||||
distorted face anxiety bloated panic shocked surprised vulnerable
|
|
||||||
🥺 pleading face begging big eyes face mercy not pleading please pretty puppy sad why
|
🥺 pleading face begging big eyes face mercy not pleading please pretty puppy sad why
|
||||||
🥹 face holding back tears admiration aww back cry embarrassed face feelings grateful gratitude holding joy please proud resist sad tears
|
🥹 face holding back tears admiration aww back cry embarrassed face feelings grateful gratitude holding joy please proud resist sad tears
|
||||||
😦 frowning face with open mouth caught face frown frowning guard mouth open scared scary surprise what wow
|
😦 frowning face with open mouth caught face frown frowning guard mouth open scared scary surprise what wow
|
||||||
@@ -206,7 +205,6 @@
|
|||||||
💋 kiss mark dating emotion heart kiss kissing lips mark romance sexy
|
💋 kiss mark dating emotion heart kiss kissing lips mark romance sexy
|
||||||
💯 hundred points 100 a+ agree clearly definitely faithful fleek full hundred keep perfect point score true truth yup
|
💯 hundred points 100 a+ agree clearly definitely faithful fleek full hundred keep perfect point score true truth yup
|
||||||
💢 anger symbol anger angry comic mad symbol upset
|
💢 anger symbol anger angry comic mad symbol upset
|
||||||
fight cloud argument brawl debate disagreement fight ruckus wrestle
|
|
||||||
💥 collision bomb boom collide comic explode
|
💥 collision bomb boom collide comic explode
|
||||||
💫 dizzy comic shining shooting star stars
|
💫 dizzy comic shining shooting star stars
|
||||||
💦 sweat droplets comic drip droplet droplets drops splashing squirt sweat water wet work workout
|
💦 sweat droplets comic drip droplet droplets drops splashing squirt sweat water wet work workout
|
||||||
@@ -451,7 +449,6 @@
|
|||||||
🧟♂️ man zombie apocalypse dead halloween horror man scary undead walking zombie
|
🧟♂️ man zombie apocalypse dead halloween horror man scary undead walking zombie
|
||||||
🧟♀️ woman zombie apocalypse dead halloween horror scary undead walking woman zombie
|
🧟♀️ woman zombie apocalypse dead halloween horror scary undead walking woman zombie
|
||||||
🧌 troll fairy fantasy monster tale trolling
|
🧌 troll fairy fantasy monster tale trolling
|
||||||
hairy creature bigfoot cryptid forest giant hairy sasquatch woodwose yeti
|
|
||||||
💆 person getting massage face getting headache massage person relax relaxing salon soothe spa tension therapy treatment
|
💆 person getting massage face getting headache massage person relax relaxing salon soothe spa tension therapy treatment
|
||||||
💆♂️ man getting massage face getting headache man massage relax relaxing salon soothe spa tension therapy treatment
|
💆♂️ man getting massage face getting headache man massage relax relaxing salon soothe spa tension therapy treatment
|
||||||
💆♀️ woman getting massage face getting headache massage relax relaxing salon soothe spa tension therapy treatment woman
|
💆♀️ woman getting massage face getting headache massage relax relaxing salon soothe spa tension therapy treatment woman
|
||||||
@@ -461,43 +458,42 @@
|
|||||||
🚶 person walking amble gait hike man pace pedestrian person stride stroll walk walking
|
🚶 person walking amble gait hike man pace pedestrian person stride stroll walk walking
|
||||||
🚶♂️ man walking amble gait hike man pace pedestrian stride stroll walk walking
|
🚶♂️ man walking amble gait hike man pace pedestrian stride stroll walk walking
|
||||||
🚶♀️ woman walking amble gait hike man pace pedestrian stride stroll walk walking woman
|
🚶♀️ woman walking amble gait hike man pace pedestrian stride stroll walk walking woman
|
||||||
🚶➡️ person walking: facing right amble facing gait hike man pace pedestrian person right stride stroll walk walking
|
🚶➡️ person walking facing right amble gait hike man pace pedestrian person stride stroll walk walking
|
||||||
🚶♀️➡️ woman walking: facing right amble facing gait hike man pace pedestrian right stride stroll walk walking woman
|
🚶♀️➡️ woman walking facing right amble gait hike man pace pedestrian stride stroll walk walking woman
|
||||||
🚶♂️➡️ man walking: facing right amble facing gait hike man pace pedestrian right stride stroll walk walking
|
🚶♂️➡️ man walking facing right amble gait hike man pace pedestrian stride stroll walk walking
|
||||||
🧍 person standing person stand standing
|
🧍 person standing person stand standing
|
||||||
🧍♂️ man standing man stand standing
|
🧍♂️ man standing man stand standing
|
||||||
🧍♀️ woman standing stand standing woman
|
🧍♀️ woman standing stand standing woman
|
||||||
🧎 person kneeling kneel kneeling knees person
|
🧎 person kneeling kneel kneeling knees person
|
||||||
🧎♂️ man kneeling kneel kneeling knees man
|
🧎♂️ man kneeling kneel kneeling knees man
|
||||||
🧎♀️ woman kneeling kneel kneeling knees woman
|
🧎♀️ woman kneeling kneel kneeling knees woman
|
||||||
🧎➡️ person kneeling: facing right facing kneel kneeling knees person right
|
🧎➡️ person kneeling facing right kneel kneeling knees person
|
||||||
🧎♀️➡️ woman kneeling: facing right facing kneel kneeling knees right woman
|
🧎♀️➡️ woman kneeling facing right kneel kneeling knees woman
|
||||||
🧎♂️➡️ man kneeling: facing right facing kneel kneeling knees man right
|
🧎♂️➡️ man kneeling facing right kneel kneeling knees man
|
||||||
🧑🦯 person with white cane accessibility blind cane person probing white
|
🧑🦯 person with white cane accessibility blind cane person probing white
|
||||||
🧑🦯➡️ person with white cane: facing right accessibility blind cane facing person probing right white
|
🧑🦯➡️ person with white cane facing right accessibility blind cane person probing white
|
||||||
👨🦯 man with white cane accessibility blind cane man probing white
|
👨🦯 man with white cane accessibility blind cane man probing white
|
||||||
👨🦯➡️ man with white cane: facing right accessibility blind cane facing man probing right white
|
👨🦯➡️ man with white cane facing right accessibility blind cane man probing white
|
||||||
👩🦯 woman with white cane accessibility blind cane probing white woman
|
👩🦯 woman with white cane accessibility blind cane probing white woman
|
||||||
👩🦯➡️ woman with white cane: facing right accessibility blind cane facing probing right white woman
|
👩🦯➡️ woman with white cane facing right accessibility blind cane probing white woman
|
||||||
🧑🦼 person in motorized wheelchair accessibility motorized person wheelchair
|
🧑🦼 person in motorized wheelchair accessibility motorized person wheelchair
|
||||||
🧑🦼➡️ person in motorized wheelchair: facing right accessibility facing motorized person right wheelchair
|
🧑🦼➡️ person in motorized wheelchair facing right accessibility motorized person wheelchair
|
||||||
👨🦼 man in motorized wheelchair accessibility man motorized wheelchair
|
👨🦼 man in motorized wheelchair accessibility man motorized wheelchair
|
||||||
👨🦼➡️ man in motorized wheelchair: facing right accessibility facing man motorized right wheelchair
|
👨🦼➡️ man in motorized wheelchair facing right accessibility man motorized wheelchair
|
||||||
👩🦼 woman in motorized wheelchair accessibility motorized wheelchair woman
|
👩🦼 woman in motorized wheelchair accessibility motorized wheelchair woman
|
||||||
👩🦼➡️ woman in motorized wheelchair: facing right accessibility facing motorized right wheelchair woman
|
👩🦼➡️ woman in motorized wheelchair facing right accessibility motorized wheelchair woman
|
||||||
🧑🦽 person in manual wheelchair accessibility manual person wheelchair
|
🧑🦽 person in manual wheelchair accessibility manual person wheelchair
|
||||||
🧑🦽➡️ person in manual wheelchair: facing right accessibility facing manual person right wheelchair
|
🧑🦽➡️ person in manual wheelchair facing right accessibility manual person wheelchair
|
||||||
👨🦽 man in manual wheelchair accessibility man manual wheelchair
|
👨🦽 man in manual wheelchair accessibility man manual wheelchair
|
||||||
👨🦽➡️ man in manual wheelchair: facing right accessibility facing man manual right wheelchair
|
👨🦽➡️ man in manual wheelchair facing right accessibility man manual wheelchair
|
||||||
👩🦽 woman in manual wheelchair accessibility manual wheelchair woman
|
👩🦽 woman in manual wheelchair accessibility manual wheelchair woman
|
||||||
👩🦽➡️ woman in manual wheelchair: facing right accessibility facing manual right wheelchair woman
|
👩🦽➡️ woman in manual wheelchair facing right accessibility manual wheelchair woman
|
||||||
🏃 person running fast hurry marathon move person quick race racing run rush speed
|
🏃 person running fast hurry marathon move person quick race racing run rush speed
|
||||||
🏃♂️ man running fast hurry man marathon move quick race racing run rush speed
|
🏃♂️ man running fast hurry man marathon move quick race racing run rush speed
|
||||||
🏃♀️ woman running fast hurry marathon move quick race racing run rush speed woman
|
🏃♀️ woman running fast hurry marathon move quick race racing run rush speed woman
|
||||||
🏃➡️ person running: facing right facing fast hurry marathon move person quick race racing right run rush speed
|
🏃➡️ person running facing right fast hurry marathon move person quick race racing run rush speed
|
||||||
🏃♀️➡️ woman running: facing right facing fast hurry marathon move quick race racing right run rush speed woman
|
🏃♀️➡️ woman running facing right fast hurry marathon move quick race racing run rush speed woman
|
||||||
🏃♂️➡️ man running: facing right facing fast hurry man marathon move quick race racing right run rush speed
|
🏃♂️➡️ man running facing right fast hurry man marathon move quick race racing run rush speed
|
||||||
🧑🩰 ballet dancer ballet dancer
|
|
||||||
💃 woman dancing dance dancer dancing elegant festive flair flamenco groove let’s salsa tango woman
|
💃 woman dancing dance dancer dancing elegant festive flair flamenco groove let’s salsa tango woman
|
||||||
🕺 man dancing dance dancer dancing elegant festive flair flamenco groove let’s man salsa tango
|
🕺 man dancing dance dancer dancing elegant festive flair flamenco groove let’s man salsa tango
|
||||||
🕴️ person in suit levitating business levitating person suit
|
🕴️ person in suit levitating business levitating person suit
|
||||||
@@ -715,7 +711,6 @@
|
|||||||
🐳 spouting whale animal beach face ocean spouting whale
|
🐳 spouting whale animal beach face ocean spouting whale
|
||||||
🐋 whale animal beach ocean
|
🐋 whale animal beach ocean
|
||||||
🐬 dolphin animal beach flipper ocean
|
🐬 dolphin animal beach flipper ocean
|
||||||
orca marine ocean whale
|
|
||||||
🦭 seal animal lion ocean sea
|
🦭 seal animal lion ocean sea
|
||||||
🐟️ fish animal dinner fishes fishing pisces zodiac
|
🐟️ fish animal dinner fishes fishing pisces zodiac
|
||||||
🐠 tropical fish animal fish fishes tropical
|
🐠 tropical fish animal fish fishes tropical
|
||||||
@@ -915,7 +910,6 @@
|
|||||||
🧭 compass direction magnetic navigation orienteering
|
🧭 compass direction magnetic navigation orienteering
|
||||||
🏔️ snow-capped mountain cold mountain snow snow-capped
|
🏔️ snow-capped mountain cold mountain snow snow-capped
|
||||||
⛰️ mountain mountain
|
⛰️ mountain mountain
|
||||||
landslide avalanche danger disaster earthquake mountain mudslide rocks
|
|
||||||
🌋 volcano eruption mountain nature
|
🌋 volcano eruption mountain nature
|
||||||
🗻 mount fuji fuji mount mountain nature
|
🗻 mount fuji fuji mount mountain nature
|
||||||
🏕️ camping camping
|
🏕️ camping camping
|
||||||
@@ -1276,11 +1270,10 @@
|
|||||||
🎧️ headphone earbud sound
|
🎧️ headphone earbud sound
|
||||||
📻️ radio entertainment tbt video
|
📻️ radio entertainment tbt video
|
||||||
🎷 saxophone instrument music sax
|
🎷 saxophone instrument music sax
|
||||||
🎺 trumpet instrument music
|
|
||||||
trombone brass instrument jazz music sad slide
|
|
||||||
🪗 accordion box concertina instrument music squeeze squeezebox
|
🪗 accordion box concertina instrument music squeeze squeezebox
|
||||||
🎸 guitar instrument music strat
|
🎸 guitar instrument music strat
|
||||||
🎹 musical keyboard instrument keyboard music musical piano
|
🎹 musical keyboard instrument keyboard music musical piano
|
||||||
|
🎺 trumpet instrument music
|
||||||
🎻 violin instrument music
|
🎻 violin instrument music
|
||||||
🪕 banjo music stringed
|
🪕 banjo music stringed
|
||||||
🥁 drum drumsticks music
|
🥁 drum drumsticks music
|
||||||
@@ -1341,9 +1334,8 @@
|
|||||||
📑 bookmark tabs bookmark mark marker tabs
|
📑 bookmark tabs bookmark mark marker tabs
|
||||||
🔖 bookmark mark
|
🔖 bookmark mark
|
||||||
🏷️ label tag
|
🏷️ label tag
|
||||||
🪙 coin dollar euro gold metal money rich silver treasure
|
|
||||||
💰️ money bag bag bank bet billion cash cost dollar gold million money moneybag paid paying pot rich win
|
💰️ money bag bag bank bet billion cash cost dollar gold million money moneybag paid paying pot rich win
|
||||||
treasure chest gem gold jewels loot money prize silver valuables wealth
|
🪙 coin dollar euro gold metal money rich silver treasure
|
||||||
💴 yen banknote bank banknote bill currency money note yen
|
💴 yen banknote bank banknote bill currency money note yen
|
||||||
💵 dollar banknote bank banknote bill currency dollar money note
|
💵 dollar banknote bank banknote bill currency dollar money note
|
||||||
💶 euro banknote 100 bank banknote bill currency euro money note rich
|
💶 euro banknote 100 bank banknote bill currency euro money note rich
|
||||||
@@ -1616,16 +1608,16 @@
|
|||||||
splatter drip holi ink liquid mess paint spill stain
|
splatter drip holi ink liquid mess paint spill stain
|
||||||
#️⃣ keycap: # keycap
|
#️⃣ keycap: # keycap
|
||||||
*️⃣ keycap: * keycap
|
*️⃣ keycap: * keycap
|
||||||
0️⃣ keycap: 0 0 keycap zero
|
0️⃣ keycap: 0 keycap
|
||||||
1️⃣ keycap: 1 1 keycap one
|
1️⃣ keycap: 1 keycap
|
||||||
2️⃣ keycap: 2 2 keycap two
|
2️⃣ keycap: 2 keycap
|
||||||
3️⃣ keycap: 3 3 keycap three
|
3️⃣ keycap: 3 keycap
|
||||||
4️⃣ keycap: 4 4 four keycap
|
4️⃣ keycap: 4 keycap
|
||||||
5️⃣ keycap: 5 5 five keycap
|
5️⃣ keycap: 5 keycap
|
||||||
6️⃣ keycap: 6 6 keycap six
|
6️⃣ keycap: 6 keycap
|
||||||
7️⃣ keycap: 7 7 keycap seven
|
7️⃣ keycap: 7 keycap
|
||||||
8️⃣ keycap: 8 8 eight keycap
|
8️⃣ keycap: 8 keycap
|
||||||
9️⃣ keycap: 9 9 keycap nine
|
9️⃣ keycap: 9 keycap
|
||||||
🔟 keycap: 10 keycap
|
🔟 keycap: 10 keycap
|
||||||
🔠 input latin uppercase abcd input latin letters uppercase
|
🔠 input latin uppercase abcd input latin letters uppercase
|
||||||
🔡 input latin lowercase abcd input latin letters lowercase
|
🔡 input latin lowercase abcd input latin letters lowercase
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
background 0a0f0f
|
|
||||||
onBackground dce8e6
|
|
||||||
surface 0a0f0f
|
|
||||||
surfaceDim 0a0f0f
|
|
||||||
surfaceBright 242e2d
|
|
||||||
surfaceContainerLowest 000000
|
|
||||||
surfaceContainerLow 0e1514
|
|
||||||
surfaceContainer 131b1a
|
|
||||||
surfaceContainerHigh 192120
|
|
||||||
surfaceContainerHighest 1d2827
|
|
||||||
onSurface dce8e6
|
|
||||||
surfaceVariant 1d2827
|
|
||||||
onSurfaceVariant a2adac
|
|
||||||
outline 6d7876
|
|
||||||
outlineVariant 3f4a49
|
|
||||||
inverseSurface f6faf9
|
|
||||||
inverseOnSurface 515655
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 9bd0cc
|
|
||||||
primary 9bd0cc
|
|
||||||
primaryDim 8ec2bf
|
|
||||||
onPrimary 0d4845
|
|
||||||
primaryContainer 255b58
|
|
||||||
onPrimaryContainer b8ede9
|
|
||||||
inversePrimary 336764
|
|
||||||
primaryFixed b7ede9
|
|
||||||
primaryFixedDim a9deda
|
|
||||||
onPrimaryFixed 0c4744
|
|
||||||
onPrimaryFixedVariant 306461
|
|
||||||
secondary b0ccc9
|
|
||||||
secondaryDim a3bebc
|
|
||||||
onSecondary 2c4543
|
|
||||||
secondaryContainer 27403e
|
|
||||||
onSecondaryContainer a9c5c2
|
|
||||||
secondaryFixed cce8e5
|
|
||||||
secondaryFixedDim bedad7
|
|
||||||
onSecondaryFixed 2b4442
|
|
||||||
onSecondaryFixedVariant 47605e
|
|
||||||
tertiary d5efff
|
|
||||||
tertiaryDim b6e3fe
|
|
||||||
onTertiary 2e5c72
|
|
||||||
tertiaryContainer b6e3fe
|
|
||||||
onTertiaryContainer 255369
|
|
||||||
tertiaryFixed b6e3fe
|
|
||||||
tertiaryFixedDim a8d5ef
|
|
||||||
onTertiaryFixed 0b4156
|
|
||||||
onTertiaryFixedVariant 2f5d73
|
|
||||||
error fa746f
|
|
||||||
errorDim c54d4a
|
|
||||||
onError 490006
|
|
||||||
errorContainer 871f21
|
|
||||||
onErrorContainer ff9993
|
|
||||||
primaryPaletteKeyColor 4c807d
|
|
||||||
secondaryPaletteKeyColor 627c7a
|
|
||||||
tertiaryPaletteKeyColor 517d94
|
|
||||||
neutralPaletteKeyColor 737877
|
|
||||||
neutralVariantPaletteKeyColor 6e7978
|
|
||||||
errorPaletteKeyColor c84f4c
|
|
||||||
primary_paletteKeyColor 4c807d
|
|
||||||
secondary_paletteKeyColor 627c7a
|
|
||||||
tertiary_paletteKeyColor 517d94
|
|
||||||
neutral_paletteKeyColor 737877
|
|
||||||
neutral_variant_paletteKeyColor 6e7978
|
|
||||||
term0 343434
|
|
||||||
term1 769e00
|
|
||||||
term2 56e2c0
|
|
||||||
term3 81fcce
|
|
||||||
term4 76b6b3
|
|
||||||
term5 7aaee9
|
|
||||||
term6 83d8c9
|
|
||||||
term7 cddcd3
|
|
||||||
term8 9aa59e
|
|
||||||
term9 85b900
|
|
||||||
term10 41f7d0
|
|
||||||
term11 cdffe9
|
|
||||||
term12 a3c8c3
|
|
||||||
term13 a2c0f7
|
|
||||||
term14 8bedd9
|
|
||||||
term15 ffffff
|
|
||||||
rosewater f1f3e5
|
|
||||||
flamingo e3e4c5
|
|
||||||
pink bae2ff
|
|
||||||
mauve 60cfe8
|
|
||||||
red 8ab5ff
|
|
||||||
maroon abbef0
|
|
||||||
peach a9daac
|
|
||||||
yellow d3fae8
|
|
||||||
green 8df1df
|
|
||||||
teal 9feee7
|
|
||||||
sky 93eae9
|
|
||||||
sapphire 70d7db
|
|
||||||
blue 57cdda
|
|
||||||
lavender 86d9e7
|
|
||||||
klink 00969e
|
|
||||||
klinkSelection 00969e
|
|
||||||
kvisited 008ca9
|
|
||||||
kvisitedSelection 008ca9
|
|
||||||
knegative 838f00
|
|
||||||
knegativeSelection 838f00
|
|
||||||
kneutral 34c359
|
|
||||||
kneutralSelection 34c359
|
|
||||||
kpositive 00beab
|
|
||||||
kpositiveSelection 00beab
|
|
||||||
text dce8e6
|
|
||||||
subtext1 a2adac
|
|
||||||
subtext0 6d7876
|
|
||||||
overlay2 5f6967
|
|
||||||
overlay1 505958
|
|
||||||
overlay0 434b4a
|
|
||||||
surface2 353d3c
|
|
||||||
surface1 282e2e
|
|
||||||
surface0 191f1e
|
|
||||||
base 0a0f0f
|
|
||||||
mantle 0a0f0f
|
|
||||||
crust 090e0e
|
|
||||||
success B5CCBA
|
|
||||||
onSuccess 213528
|
|
||||||
successContainer 374B3E
|
|
||||||
onSuccessContainer D1E9D6
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
background f6faf9
|
|
||||||
onBackground 2a3433
|
|
||||||
surface f6faf9
|
|
||||||
surfaceDim d1dcdb
|
|
||||||
surfaceBright f6faf9
|
|
||||||
surfaceContainerLowest ffffff
|
|
||||||
surfaceContainerLow eef5f3
|
|
||||||
surfaceContainer e7f0ee
|
|
||||||
surfaceContainerHigh e1eae8
|
|
||||||
surfaceContainerHighest d9e5e3
|
|
||||||
onSurface 2a3433
|
|
||||||
surfaceVariant d9e5e3
|
|
||||||
onSurfaceVariant 566160
|
|
||||||
outline 727d7c
|
|
||||||
outlineVariant a9b4b3
|
|
||||||
inverseSurface 0a0f0f
|
|
||||||
inverseOnSurface 999e9d
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 1c6a66
|
|
||||||
primary 1c6a66
|
|
||||||
primaryDim 045d5a
|
|
||||||
onPrimary e1fffc
|
|
||||||
primaryContainer a8f0eb
|
|
||||||
onPrimaryContainer 015c59
|
|
||||||
inversePrimary b0f8f3
|
|
||||||
primaryFixed a8f0eb
|
|
||||||
primaryFixedDim 9ae1dc
|
|
||||||
onPrimaryFixed 004845
|
|
||||||
onPrimaryFixedVariant 166663
|
|
||||||
secondary 4a6462
|
|
||||||
secondaryDim 3e5856
|
|
||||||
onSecondary e2fffc
|
|
||||||
secondaryContainer cce8e5
|
|
||||||
onSecondaryContainer 3d5654
|
|
||||||
secondaryFixed cce8e5
|
|
||||||
secondaryFixedDim bedad7
|
|
||||||
onSecondaryFixed 2b4442
|
|
||||||
onSecondaryFixedVariant 47605e
|
|
||||||
tertiary 37647b
|
|
||||||
tertiaryDim 2a586e
|
|
||||||
onTertiary f4faff
|
|
||||||
tertiaryContainer b6e3fe
|
|
||||||
onTertiaryContainer 255369
|
|
||||||
tertiaryFixed b6e3fe
|
|
||||||
tertiaryFixedDim a8d5ef
|
|
||||||
onTertiaryFixed 0b4156
|
|
||||||
onTertiaryFixedVariant 2f5d73
|
|
||||||
error a83836
|
|
||||||
errorDim 67040d
|
|
||||||
onError fff7f6
|
|
||||||
errorContainer fa746f
|
|
||||||
onErrorContainer 6e0a12
|
|
||||||
primaryPaletteKeyColor 3a827e
|
|
||||||
secondaryPaletteKeyColor 627c7a
|
|
||||||
tertiaryPaletteKeyColor 517d94
|
|
||||||
neutralPaletteKeyColor 737877
|
|
||||||
neutralVariantPaletteKeyColor 6e7978
|
|
||||||
errorPaletteKeyColor c84f4c
|
|
||||||
primary_paletteKeyColor 3a827e
|
|
||||||
secondary_paletteKeyColor 627c7a
|
|
||||||
tertiary_paletteKeyColor 517d94
|
|
||||||
neutral_paletteKeyColor 737877
|
|
||||||
neutral_variant_paletteKeyColor 6e7978
|
|
||||||
term0 9a9b99
|
|
||||||
term1 005bcc
|
|
||||||
term2 00907c
|
|
||||||
term3 427d3b
|
|
||||||
term4 269a7a
|
|
||||||
term5 0071a3
|
|
||||||
term6 128f8d
|
|
||||||
term7 1f2324
|
|
||||||
term8 0f0f0f
|
|
||||||
term9 0071fa
|
|
||||||
term10 00b49c
|
|
||||||
term11 5d9954
|
|
||||||
term12 52be9c
|
|
||||||
term13 008cca
|
|
||||||
term14 45b0ae
|
|
||||||
term15 25292a
|
|
||||||
rosewater 6b8647
|
|
||||||
flamingo 6f7c1e
|
|
||||||
pink 0085c0
|
|
||||||
mauve 005d6c
|
|
||||||
red 515900
|
|
||||||
maroon 606c00
|
|
||||||
peach 198900
|
|
||||||
yellow 008f67
|
|
||||||
green 007d6d
|
|
||||||
teal 007573
|
|
||||||
sky 00878d
|
|
||||||
sapphire 008080
|
|
||||||
blue 00636d
|
|
||||||
lavender 007e8b
|
|
||||||
klink 00969d
|
|
||||||
klinkSelection 00969e
|
|
||||||
kvisited 008ca9
|
|
||||||
kvisitedSelection 008ca9
|
|
||||||
knegative 838f00
|
|
||||||
knegativeSelection 838f00
|
|
||||||
kneutral 34c359
|
|
||||||
kneutralSelection 34c359
|
|
||||||
kpositive 00beab
|
|
||||||
kpositiveSelection 00beac
|
|
||||||
text 2a3433
|
|
||||||
subtext1 566160
|
|
||||||
subtext0 727d7c
|
|
||||||
overlay2 828c8b
|
|
||||||
overlay1 949d9c
|
|
||||||
overlay0 a5aead
|
|
||||||
surface2 b8bfbe
|
|
||||||
surface1 cbd1d0
|
|
||||||
surface0 e1e6e5
|
|
||||||
base f6faf9
|
|
||||||
mantle eef1f0
|
|
||||||
crust e9eceb
|
|
||||||
success 4F6354
|
|
||||||
onSuccess FFFFFF
|
|
||||||
successContainer D1E8D5
|
|
||||||
onSuccessContainer 0C1F13
|
|
||||||
@@ -82,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 33653E
|
|
||||||
secondary_paletteKeyColor 1B4E2A
|
|
||||||
tertiary_paletteKeyColor 376942
|
|
||||||
neutral_paletteKeyColor 1E1E26
|
|
||||||
neutral_variant_paletteKeyColor 23252D
|
|
||||||
background 23262D
|
|
||||||
onBackground F5F5F6
|
|
||||||
surface 050505
|
|
||||||
surfaceDim 1E1E24
|
|
||||||
surfaceBright 1E1E24
|
|
||||||
surfaceContainerLowest 0a0a0b
|
|
||||||
surfaceContainerLow 0a0a0b
|
|
||||||
surfaceContainer 0a0a0b
|
|
||||||
surfaceContainerHigh 050505
|
|
||||||
surfaceContainerHighest 0f1210
|
|
||||||
onSurface F5F5F6
|
|
||||||
surfaceVariant 0a0a0b
|
|
||||||
onSurfaceVariant c9c9c9
|
|
||||||
inverseSurface 0a0a0b
|
|
||||||
inverseOnSurface ACACAC
|
|
||||||
outline 838383
|
|
||||||
outlineVariant 1E1E25
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 24BD5C
|
|
||||||
primary 24BD5C
|
|
||||||
onPrimary 091f11
|
|
||||||
primaryContainer 0f1210
|
|
||||||
onPrimaryContainer 24BD5C
|
|
||||||
inversePrimary 24BD5C
|
|
||||||
secondary 24BD5C
|
|
||||||
onSecondary 043a14
|
|
||||||
secondaryContainer 0f1210
|
|
||||||
onSecondaryContainer F4F3F5
|
|
||||||
tertiary 32653E
|
|
||||||
onTertiary F5F4F6
|
|
||||||
tertiaryContainer 1E1E25
|
|
||||||
onTertiaryContainer F5F5F6
|
|
||||||
error c66e73
|
|
||||||
onError F5F4F6
|
|
||||||
errorContainer 893034
|
|
||||||
onErrorContainer F5F4F6
|
|
||||||
primaryFixed 24BD5C
|
|
||||||
primaryFixedDim 24BD5C
|
|
||||||
onPrimaryFixed F5F4F6
|
|
||||||
onPrimaryFixedVariant F5F4F6
|
|
||||||
secondaryFixed 24BD5C
|
|
||||||
secondaryFixedDim 24BD5C
|
|
||||||
onSecondaryFixed F5F4F6
|
|
||||||
onSecondaryFixedVariant F5F4F6
|
|
||||||
tertiaryFixed 24BD5C
|
|
||||||
tertiaryFixedDim 24BD5C
|
|
||||||
onTertiaryFixed F5F4F6
|
|
||||||
onTertiaryFixedVariant F5F4F6
|
|
||||||
term0 343434
|
|
||||||
term1 23B65A
|
|
||||||
term2 43ff88
|
|
||||||
term3 7cfcab
|
|
||||||
term4 78c19f
|
|
||||||
term5 7ae9a7
|
|
||||||
term6 80deb2
|
|
||||||
term7 ccdcd6
|
|
||||||
term8 9aa59f
|
|
||||||
term9 cdff9e
|
|
||||||
term10 00f608
|
|
||||||
term11 c9fff3
|
|
||||||
term12 a4c7cd
|
|
||||||
term13 a5f7a2
|
|
||||||
term14 87f1b5
|
|
||||||
term15 ffffff
|
|
||||||
rosewater f4f0fa
|
|
||||||
flamingo dfe0f5
|
|
||||||
pink bdffd4
|
|
||||||
mauve 73fa90
|
|
||||||
red 8affab
|
|
||||||
maroon abf0c5
|
|
||||||
peach a9daac
|
|
||||||
yellow d0f9f4
|
|
||||||
green 8af797
|
|
||||||
teal a0f9aa
|
|
||||||
sky cefb97
|
|
||||||
sapphire 85ef77
|
|
||||||
blue 65eea0
|
|
||||||
lavender 90f79e
|
|
||||||
klink 65eea0
|
|
||||||
klinkSelection 65eea0
|
|
||||||
kvisited 73fa90
|
|
||||||
kvisitedSelection 73fa90
|
|
||||||
knegative 8affab
|
|
||||||
knegativeSelection 8affab
|
|
||||||
kneutral d0f9f4
|
|
||||||
kneutralSelection d0f9f4
|
|
||||||
kpositive 8af797
|
|
||||||
kpositiveSelection 8af797
|
|
||||||
text e0e3e4
|
|
||||||
subtext1 bec8cc
|
|
||||||
subtext0 889296
|
|
||||||
overlay2 767f83
|
|
||||||
overlay1 646c6f
|
|
||||||
overlay0 535a5d
|
|
||||||
surface2 43494b
|
|
||||||
surface1 33383a
|
|
||||||
surface0 212627
|
|
||||||
base 101415
|
|
||||||
mantle 101415
|
|
||||||
crust 0f1314
|
|
||||||
success B5CCBA
|
|
||||||
onSuccess 213528
|
|
||||||
successContainer 374B3E
|
|
||||||
onSuccessContainer D1E9D6
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 33653E
|
|
||||||
secondary_paletteKeyColor 1B4E2A
|
|
||||||
tertiary_paletteKeyColor 376942
|
|
||||||
neutral_paletteKeyColor 1E1E26
|
|
||||||
neutral_variant_paletteKeyColor 23252D
|
|
||||||
background 23262D
|
|
||||||
onBackground F5F5F6
|
|
||||||
surface 1E1E24
|
|
||||||
surfaceDim 1E1E24
|
|
||||||
surfaceBright 1E1E24
|
|
||||||
surfaceContainerLowest 23262C
|
|
||||||
surfaceContainerLow 23262C
|
|
||||||
surfaceContainer 23262C
|
|
||||||
surfaceContainerHigh 1b1d22
|
|
||||||
surfaceContainerHighest 232C29
|
|
||||||
onSurface F5F5F6
|
|
||||||
surfaceVariant 23262C
|
|
||||||
onSurfaceVariant c9c9c9
|
|
||||||
inverseSurface 23262C
|
|
||||||
inverseOnSurface ACACAC
|
|
||||||
outline 979797
|
|
||||||
outlineVariant 1E1E25
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 24BD5C
|
|
||||||
primary 24BD5C
|
|
||||||
onPrimary 091f11
|
|
||||||
primaryContainer 232c29
|
|
||||||
onPrimaryContainer 24BD5C
|
|
||||||
inversePrimary 24BD5C
|
|
||||||
secondary 24BD5C
|
|
||||||
onSecondary 043a14
|
|
||||||
secondaryContainer 232c29
|
|
||||||
onSecondaryContainer F4F3F5
|
|
||||||
tertiary 32653E
|
|
||||||
onTertiary F5F4F6
|
|
||||||
tertiaryContainer 1E1E25
|
|
||||||
onTertiaryContainer F5F5F6
|
|
||||||
error c66e73
|
|
||||||
onError F5F4F6
|
|
||||||
errorContainer 893034
|
|
||||||
onErrorContainer F5F4F6
|
|
||||||
primaryFixed 24BD5C
|
|
||||||
primaryFixedDim 24BD5C
|
|
||||||
onPrimaryFixed F5F4F6
|
|
||||||
onPrimaryFixedVariant F5F4F6
|
|
||||||
secondaryFixed 24BD5C
|
|
||||||
secondaryFixedDim 24BD5C
|
|
||||||
onSecondaryFixed F5F4F6
|
|
||||||
onSecondaryFixedVariant F5F4F6
|
|
||||||
tertiaryFixed 24BD5C
|
|
||||||
tertiaryFixedDim 24BD5C
|
|
||||||
onTertiaryFixed F5F4F6
|
|
||||||
onTertiaryFixedVariant F5F4F6
|
|
||||||
term0 343434
|
|
||||||
term1 23B65A
|
|
||||||
term2 43ff88
|
|
||||||
term3 7cfcab
|
|
||||||
term4 78c19f
|
|
||||||
term5 7ae9a7
|
|
||||||
term6 80deb2
|
|
||||||
term7 ccdcd6
|
|
||||||
term8 9aa59f
|
|
||||||
term9 cdff9e
|
|
||||||
term10 00f608
|
|
||||||
term11 c9fff3
|
|
||||||
term12 a4c7cd
|
|
||||||
term13 a5f7a2
|
|
||||||
term14 87f1b5
|
|
||||||
term15 ffffff
|
|
||||||
rosewater f4f0fa
|
|
||||||
flamingo dfe0f5
|
|
||||||
pink bdffd4
|
|
||||||
mauve 73fa90
|
|
||||||
red 8affab
|
|
||||||
maroon abf0c5
|
|
||||||
peach a9daac
|
|
||||||
yellow d0f9f4
|
|
||||||
green 8af797
|
|
||||||
teal a0f9aa
|
|
||||||
sky cefb97
|
|
||||||
sapphire 85ef77
|
|
||||||
blue 65eea0
|
|
||||||
lavender 90f79e
|
|
||||||
klink 65eea0
|
|
||||||
klinkSelection 65eea0
|
|
||||||
kvisited 73fa90
|
|
||||||
kvisitedSelection 73fa90
|
|
||||||
knegative 8affab
|
|
||||||
knegativeSelection 8affab
|
|
||||||
kneutral d0f9f4
|
|
||||||
kneutralSelection d0f9f4
|
|
||||||
kpositive 8af797
|
|
||||||
kpositiveSelection 8af797
|
|
||||||
text e0e3e4
|
|
||||||
subtext1 bec8cc
|
|
||||||
subtext0 889296
|
|
||||||
overlay2 767f83
|
|
||||||
overlay1 646c6f
|
|
||||||
overlay0 535a5d
|
|
||||||
surface2 43494b
|
|
||||||
surface1 33383a
|
|
||||||
surface0 212627
|
|
||||||
base 101415
|
|
||||||
mantle 101415
|
|
||||||
crust 0f1314
|
|
||||||
success B5CCBA
|
|
||||||
onSuccess 213528
|
|
||||||
successContainer 374B3E
|
|
||||||
onSuccessContainer D1E9D6
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor BD93F9
|
|
||||||
secondary_paletteKeyColor 50FA7B
|
|
||||||
tertiary_paletteKeyColor FF79C6
|
|
||||||
neutral_paletteKeyColor 282A36
|
|
||||||
neutral_variant_paletteKeyColor 44475A
|
|
||||||
background 282A36
|
|
||||||
onBackground F8F8F2
|
|
||||||
surface 343746
|
|
||||||
surfaceDim 21222C
|
|
||||||
surfaceBright 4D4F66
|
|
||||||
surfaceContainerLowest 191A21
|
|
||||||
surfaceContainerLow 3C3F4E
|
|
||||||
surfaceContainer 3E4153
|
|
||||||
surfaceContainerHigh 4D4F66
|
|
||||||
surfaceContainerHighest 565970
|
|
||||||
onSurface F8F8F2
|
|
||||||
surfaceVariant 3E4153
|
|
||||||
onSurfaceVariant F8F8F2
|
|
||||||
inverseSurface F8F8F2
|
|
||||||
inverseOnSurface 282A36
|
|
||||||
outline 6272A4
|
|
||||||
outlineVariant 4D4F66
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint BD93F9
|
|
||||||
primary BD93F9
|
|
||||||
onPrimary 282A36
|
|
||||||
primaryContainer 4D4F66
|
|
||||||
onPrimaryContainer BD93F9
|
|
||||||
inversePrimary 9D73D9
|
|
||||||
secondary 50FA7B
|
|
||||||
onSecondary 282A36
|
|
||||||
secondaryContainer 4D4F66
|
|
||||||
onSecondaryContainer 50FA7B
|
|
||||||
tertiary FF79C6
|
|
||||||
onTertiary 282A36
|
|
||||||
tertiaryContainer 4D4F66
|
|
||||||
onTertiaryContainer FF79C6
|
|
||||||
error FF5555
|
|
||||||
onError 282A36
|
|
||||||
errorContainer 4C3743
|
|
||||||
onErrorContainer FF5555
|
|
||||||
primaryFixed BD93F9
|
|
||||||
primaryFixedDim 9D73D9
|
|
||||||
onPrimaryFixed 282A36
|
|
||||||
onPrimaryFixedVariant 3E4153
|
|
||||||
secondaryFixed 50FA7B
|
|
||||||
secondaryFixedDim 30DA5B
|
|
||||||
onSecondaryFixed 282A36
|
|
||||||
onSecondaryFixedVariant 3E4153
|
|
||||||
tertiaryFixed FF79C6
|
|
||||||
tertiaryFixedDim DF59A6
|
|
||||||
onTertiaryFixed 282A36
|
|
||||||
onTertiaryFixedVariant 3E4153
|
|
||||||
term0 282A36
|
|
||||||
term1 FF5555
|
|
||||||
term2 50FA7B
|
|
||||||
term3 F1FA8C
|
|
||||||
term4 BD93F9
|
|
||||||
term5 FF79C6
|
|
||||||
term6 8BE9FD
|
|
||||||
term7 F8F8F2
|
|
||||||
term8 6272A4
|
|
||||||
term9 FF6E6E
|
|
||||||
term10 69FF94
|
|
||||||
term11 FFFFA5
|
|
||||||
term12 D6ACFF
|
|
||||||
term13 FF92DF
|
|
||||||
term14 A4FFFF
|
|
||||||
term15 FFFFFF
|
|
||||||
rosewater F8F8F2
|
|
||||||
flamingo FFB86C
|
|
||||||
pink FF79C6
|
|
||||||
mauve BD93F9
|
|
||||||
red FF5555
|
|
||||||
maroon FF6E6E
|
|
||||||
peach FFB86C
|
|
||||||
yellow F1FA8C
|
|
||||||
green 50FA7B
|
|
||||||
teal 8BE9FD
|
|
||||||
sky 8BE9FD
|
|
||||||
sapphire 8BE9FD
|
|
||||||
blue BD93F9
|
|
||||||
lavender BD93F9
|
|
||||||
klink BD93F9
|
|
||||||
klinkSelection BD93F9
|
|
||||||
kvisited FF79C6
|
|
||||||
kvisitedSelection FF79C6
|
|
||||||
knegative FF5555
|
|
||||||
knegativeSelection FF5555
|
|
||||||
kneutral F1FA8C
|
|
||||||
kneutralSelection F1FA8C
|
|
||||||
kpositive 50FA7B
|
|
||||||
kpositiveSelection 50FA7B
|
|
||||||
text F8F8F2
|
|
||||||
subtext1 F8F8F2
|
|
||||||
subtext0 E6E6E6
|
|
||||||
overlay2 A0A0A0
|
|
||||||
overlay1 8A8A8A
|
|
||||||
overlay0 6272A4
|
|
||||||
surface2 3E4153
|
|
||||||
surface1 343746
|
|
||||||
surface0 282A36
|
|
||||||
base 282A36
|
|
||||||
mantle 21222C
|
|
||||||
crust 191A21
|
|
||||||
success 50FA7B
|
|
||||||
onSuccess 282A36
|
|
||||||
successContainer 4D4F66
|
|
||||||
onSuccessContainer F8F8F2
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 8CCFB0
|
|
||||||
secondary_paletteKeyColor E5C76B
|
|
||||||
tertiary_paletteKeyColor E5A5C5
|
|
||||||
neutral_paletteKeyColor 2D3139
|
|
||||||
neutral_variant_paletteKeyColor 3A3F4B
|
|
||||||
background 141B1E
|
|
||||||
onBackground E8E8E8
|
|
||||||
surface 232A2D
|
|
||||||
surfaceDim 0F1416
|
|
||||||
surfaceBright 3A4145
|
|
||||||
surfaceContainerLowest 0A0E10
|
|
||||||
surfaceContainerLow 2A3235
|
|
||||||
surfaceContainer 2E3538
|
|
||||||
surfaceContainerHigh 3A4145
|
|
||||||
surfaceContainerHighest 434A4E
|
|
||||||
onSurface E8E8E8
|
|
||||||
surfaceVariant 2E3538
|
|
||||||
onSurfaceVariant B3B9BE
|
|
||||||
inverseSurface E8E8E8
|
|
||||||
inverseOnSurface 141B1E
|
|
||||||
outline 8A8F94
|
|
||||||
outlineVariant 3A4145
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 8CCFB0
|
|
||||||
primary 8CCFB0
|
|
||||||
onPrimary 141B1E
|
|
||||||
primaryContainer 3A4145
|
|
||||||
onPrimaryContainer 8CCFB0
|
|
||||||
inversePrimary 6FA98C
|
|
||||||
secondary E5C76B
|
|
||||||
onSecondary 141B1E
|
|
||||||
secondaryContainer 3A4145
|
|
||||||
onSecondaryContainer E5C76B
|
|
||||||
tertiary E5A5C5
|
|
||||||
onTertiary 141B1E
|
|
||||||
tertiaryContainer 3A4145
|
|
||||||
onTertiaryContainer E5A5C5
|
|
||||||
error E57474
|
|
||||||
onError 141B1E
|
|
||||||
errorContainer 4A2C2C
|
|
||||||
onErrorContainer E57474
|
|
||||||
primaryFixed 8CCFB0
|
|
||||||
primaryFixedDim 6FA98C
|
|
||||||
onPrimaryFixed 141B1E
|
|
||||||
onPrimaryFixedVariant 3A3F4B
|
|
||||||
secondaryFixed E5C76B
|
|
||||||
secondaryFixedDim C4A855
|
|
||||||
onSecondaryFixed 141B1E
|
|
||||||
onSecondaryFixedVariant 3A3F4B
|
|
||||||
tertiaryFixed E5A5C5
|
|
||||||
tertiaryFixedDim C888A4
|
|
||||||
onTertiaryFixed 141B1E
|
|
||||||
onTertiaryFixedVariant 3A3F4B
|
|
||||||
term0 141B1E
|
|
||||||
term1 E57474
|
|
||||||
term2 8CCFB0
|
|
||||||
term3 E5C76B
|
|
||||||
term4 67B0E8
|
|
||||||
term5 C47FD5
|
|
||||||
term6 6CBFBF
|
|
||||||
term7 E8E8E8
|
|
||||||
term8 8A8F94
|
|
||||||
term9 E57474
|
|
||||||
term10 8CCFB0
|
|
||||||
term11 E5C76B
|
|
||||||
term12 67B0E8
|
|
||||||
term13 C47FD5
|
|
||||||
term14 6CBFBF
|
|
||||||
term15 E8E8E8
|
|
||||||
rosewater E8E8E8
|
|
||||||
flamingo E5A5C5
|
|
||||||
pink E5A5C5
|
|
||||||
mauve C47FD5
|
|
||||||
red E57474
|
|
||||||
maroon E57474
|
|
||||||
peach E59A84
|
|
||||||
yellow E5C76B
|
|
||||||
green 8CCFB0
|
|
||||||
teal 6CBFBF
|
|
||||||
sky 67B0E8
|
|
||||||
sapphire 67B0E8
|
|
||||||
blue 67B0E8
|
|
||||||
lavender 67B0E8
|
|
||||||
klink 67B0E8
|
|
||||||
klinkSelection 67B0E8
|
|
||||||
kvisited C47FD5
|
|
||||||
kvisitedSelection C47FD5
|
|
||||||
knegative E57474
|
|
||||||
knegativeSelection E57474
|
|
||||||
kneutral E5C76B
|
|
||||||
kneutralSelection E5C76B
|
|
||||||
kpositive 8CCFB0
|
|
||||||
kpositiveSelection 8CCFB0
|
|
||||||
text E8E8E8
|
|
||||||
subtext1 B3B9BE
|
|
||||||
subtext0 8A8F94
|
|
||||||
overlay2 7A7F84
|
|
||||||
overlay1 6A6F74
|
|
||||||
overlay0 5A5F64
|
|
||||||
surface2 2E3538
|
|
||||||
surface1 232A2D
|
|
||||||
surface0 1A2023
|
|
||||||
base 141B1E
|
|
||||||
mantle 0F1416
|
|
||||||
crust 0A0E10
|
|
||||||
success 8CCFB0
|
|
||||||
onSuccess 141B1E
|
|
||||||
successContainer 3A4145
|
|
||||||
onSuccessContainer E8E8E8
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 7FBBB3
|
|
||||||
secondary_paletteKeyColor 83C092
|
|
||||||
tertiary_paletteKeyColor A7C080
|
|
||||||
neutral_paletteKeyColor 2E383C
|
|
||||||
neutral_variant_paletteKeyColor 374145
|
|
||||||
background 1E2326
|
|
||||||
onBackground D3C6AA
|
|
||||||
surface 252B2E
|
|
||||||
surfaceDim 15191C
|
|
||||||
surfaceBright 343E43
|
|
||||||
surfaceContainerLowest 11161A
|
|
||||||
surfaceContainerLow 2A3338
|
|
||||||
surfaceContainer 2E383C
|
|
||||||
surfaceContainerHigh 343E43
|
|
||||||
surfaceContainerHighest 3A4448
|
|
||||||
onSurface D3C6AA
|
|
||||||
surfaceVariant 374145
|
|
||||||
onSurfaceVariant 9DA9A0
|
|
||||||
inverseSurface D3C6AA
|
|
||||||
inverseOnSurface 1E2326
|
|
||||||
outline 859289
|
|
||||||
outlineVariant 414B50
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 7FBBB3
|
|
||||||
primary 7FBBB3
|
|
||||||
onPrimary 1E2326
|
|
||||||
primaryContainer 414B50
|
|
||||||
onPrimaryContainer A7C080
|
|
||||||
inversePrimary 5A9A8F
|
|
||||||
secondary 83C092
|
|
||||||
onSecondary 1E2326
|
|
||||||
secondaryContainer 414B50
|
|
||||||
onSecondaryContainer A7C080
|
|
||||||
tertiary A7C080
|
|
||||||
onTertiary 1E2326
|
|
||||||
tertiaryContainer 414B50
|
|
||||||
onTertiaryContainer D3C6AA
|
|
||||||
error E67E80
|
|
||||||
onError 1E2326
|
|
||||||
errorContainer 4C3743
|
|
||||||
onErrorContainer E67E80
|
|
||||||
primaryFixed 7FBBB3
|
|
||||||
primaryFixedDim 5A9A8F
|
|
||||||
onPrimaryFixed 1E2326
|
|
||||||
onPrimaryFixedVariant 374145
|
|
||||||
secondaryFixed 83C092
|
|
||||||
secondaryFixedDim 5F8C6F
|
|
||||||
onSecondaryFixed 1E2326
|
|
||||||
onSecondaryFixedVariant 374145
|
|
||||||
tertiaryFixed A7C080
|
|
||||||
tertiaryFixedDim 7F9D5F
|
|
||||||
onTertiaryFixed 1E2326
|
|
||||||
onTertiaryFixedVariant 374145
|
|
||||||
term0 1E2326
|
|
||||||
term1 E67E80
|
|
||||||
term2 A7C080
|
|
||||||
term3 DBBC7F
|
|
||||||
term4 7FBBB3
|
|
||||||
term5 D699B6
|
|
||||||
term6 83C092
|
|
||||||
term7 D3C6AA
|
|
||||||
term8 859289
|
|
||||||
term9 E67E80
|
|
||||||
term10 A7C080
|
|
||||||
term11 DBBC7F
|
|
||||||
term12 7FBBB3
|
|
||||||
term13 D699B6
|
|
||||||
term14 83C092
|
|
||||||
term15 D3C6AA
|
|
||||||
rosewater D3C6AA
|
|
||||||
flamingo D699B6
|
|
||||||
pink D699B6
|
|
||||||
mauve D699B6
|
|
||||||
red E67E80
|
|
||||||
maroon E67E80
|
|
||||||
peach E69875
|
|
||||||
yellow DBBC7F
|
|
||||||
green A7C080
|
|
||||||
teal 83C092
|
|
||||||
sky 7FBBB3
|
|
||||||
sapphire 7FBBB3
|
|
||||||
blue 7FBBB3
|
|
||||||
lavender 7FBBB3
|
|
||||||
klink 7FBBB3
|
|
||||||
klinkSelection 7FBBB3
|
|
||||||
kvisited 83C092
|
|
||||||
kvisitedSelection 83C092
|
|
||||||
knegative E67E80
|
|
||||||
knegativeSelection E67E80
|
|
||||||
kneutral DBBC7F
|
|
||||||
kneutralSelection DBBC7F
|
|
||||||
kpositive A7C080
|
|
||||||
kpositiveSelection A7C080
|
|
||||||
text D3C6AA
|
|
||||||
subtext1 9DA9A0
|
|
||||||
subtext0 859289
|
|
||||||
overlay2 7A8478
|
|
||||||
overlay1 6F7A6F
|
|
||||||
overlay0 5F6A5F
|
|
||||||
surface2 2E383C
|
|
||||||
surface1 252B2E
|
|
||||||
surface0 1E2326
|
|
||||||
base 1E2326
|
|
||||||
mantle 15191C
|
|
||||||
crust 11161A
|
|
||||||
success A7C080
|
|
||||||
onSuccess 1E2326
|
|
||||||
successContainer 414B50
|
|
||||||
onSuccessContainer D3C6AA
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 7FBBB3
|
|
||||||
secondary_paletteKeyColor 83C092
|
|
||||||
tertiary_paletteKeyColor A7C080
|
|
||||||
neutral_paletteKeyColor 2E383C
|
|
||||||
neutral_variant_paletteKeyColor 374145
|
|
||||||
background 2D353B
|
|
||||||
onBackground D3C6AA
|
|
||||||
surface 343F44
|
|
||||||
surfaceDim 232A2E
|
|
||||||
surfaceBright 475258
|
|
||||||
surfaceContainerLowest 1E2326
|
|
||||||
surfaceContainerLow 3B474E
|
|
||||||
surfaceContainer 3D484D
|
|
||||||
surfaceContainerHigh 475258
|
|
||||||
surfaceContainerHighest 4C5258
|
|
||||||
onSurface D3C6AA
|
|
||||||
surfaceVariant 3D484D
|
|
||||||
onSurfaceVariant 9DA9A0
|
|
||||||
inverseSurface D3C6AA
|
|
||||||
inverseOnSurface 2D353B
|
|
||||||
outline 859289
|
|
||||||
outlineVariant 475258
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 7FBBB3
|
|
||||||
primary 7FBBB3
|
|
||||||
onPrimary 2D353B
|
|
||||||
primaryContainer 475258
|
|
||||||
onPrimaryContainer A7C080
|
|
||||||
inversePrimary 5A9A8F
|
|
||||||
secondary 83C092
|
|
||||||
onSecondary 2D353B
|
|
||||||
secondaryContainer 475258
|
|
||||||
onSecondaryContainer A7C080
|
|
||||||
tertiary A7C080
|
|
||||||
onTertiary 2D353B
|
|
||||||
tertiaryContainer 475258
|
|
||||||
onTertiaryContainer D3C6AA
|
|
||||||
error E67E80
|
|
||||||
onError 2D353B
|
|
||||||
errorContainer 4C3743
|
|
||||||
onErrorContainer E67E80
|
|
||||||
primaryFixed 7FBBB3
|
|
||||||
primaryFixedDim 5A9A8F
|
|
||||||
onPrimaryFixed 2D353B
|
|
||||||
onPrimaryFixedVariant 374145
|
|
||||||
secondaryFixed 83C092
|
|
||||||
secondaryFixedDim 5F8C6F
|
|
||||||
onSecondaryFixed 2D353B
|
|
||||||
onSecondaryFixedVariant 374145
|
|
||||||
tertiaryFixed A7C080
|
|
||||||
tertiaryFixedDim 7F9D5F
|
|
||||||
onTertiaryFixed 2D353B
|
|
||||||
onTertiaryFixedVariant 374145
|
|
||||||
term0 2D353B
|
|
||||||
term1 E67E80
|
|
||||||
term2 A7C080
|
|
||||||
term3 DBBC7F
|
|
||||||
term4 7FBBB3
|
|
||||||
term5 D699B6
|
|
||||||
term6 83C092
|
|
||||||
term7 D3C6AA
|
|
||||||
term8 859289
|
|
||||||
term9 E67E80
|
|
||||||
term10 A7C080
|
|
||||||
term11 DBBC7F
|
|
||||||
term12 7FBBB3
|
|
||||||
term13 D699B6
|
|
||||||
term14 83C092
|
|
||||||
term15 D3C6AA
|
|
||||||
rosewater D3C6AA
|
|
||||||
flamingo D699B6
|
|
||||||
pink D699B6
|
|
||||||
mauve D699B6
|
|
||||||
red E67E80
|
|
||||||
maroon E67E80
|
|
||||||
peach E69875
|
|
||||||
yellow DBBC7F
|
|
||||||
green A7C080
|
|
||||||
teal 83C092
|
|
||||||
sky 7FBBB3
|
|
||||||
sapphire 7FBBB3
|
|
||||||
blue 7FBBB3
|
|
||||||
lavender 7FBBB3
|
|
||||||
klink 7FBBB3
|
|
||||||
klinkSelection 7FBBB3
|
|
||||||
kvisited 83C092
|
|
||||||
kvisitedSelection 83C092
|
|
||||||
knegative E67E80
|
|
||||||
knegativeSelection E67E80
|
|
||||||
kneutral DBBC7F
|
|
||||||
kneutralSelection DBBC7F
|
|
||||||
kpositive A7C080
|
|
||||||
kpositiveSelection A7C080
|
|
||||||
text D3C6AA
|
|
||||||
subtext1 9DA9A0
|
|
||||||
subtext0 859289
|
|
||||||
overlay2 7A8478
|
|
||||||
overlay1 6F7A6F
|
|
||||||
overlay0 5F6A5F
|
|
||||||
surface2 3D484D
|
|
||||||
surface1 343F44
|
|
||||||
surface0 2D353B
|
|
||||||
base 2D353B
|
|
||||||
mantle 232A2E
|
|
||||||
crust 1E2326
|
|
||||||
success A7C080
|
|
||||||
onSuccess 2D353B
|
|
||||||
successContainer 475258
|
|
||||||
onSuccessContainer D3C6AA
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 3A94C5
|
|
||||||
secondary_paletteKeyColor 35A77C
|
|
||||||
tertiary_paletteKeyColor 8DA101
|
|
||||||
neutral_paletteKeyColor E6E2CC
|
|
||||||
neutral_variant_paletteKeyColor E0DCC7
|
|
||||||
background FDF6E3
|
|
||||||
onBackground 5C6A72
|
|
||||||
surface F3EAD3
|
|
||||||
surfaceDim FDF6E3
|
|
||||||
surfaceBright FFFBF0
|
|
||||||
surfaceContainerLowest FFFBF0
|
|
||||||
surfaceContainerLow FDF6E3
|
|
||||||
surfaceContainer F3EAD3
|
|
||||||
surfaceContainerHigh EAE4CA
|
|
||||||
surfaceContainerHighest E0DCC7
|
|
||||||
onSurface 5C6A72
|
|
||||||
surfaceVariant EAE4CA
|
|
||||||
onSurfaceVariant 6F7C84
|
|
||||||
inverseSurface 5C6A72
|
|
||||||
inverseOnSurface FDF6E3
|
|
||||||
outline 939F91
|
|
||||||
outlineVariant E0DCC7
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 3A94C5
|
|
||||||
primary 3A94C5
|
|
||||||
onPrimary FFFBF0
|
|
||||||
primaryContainer E0DCC7
|
|
||||||
onPrimaryContainer 8DA101
|
|
||||||
inversePrimary 5FAFD7
|
|
||||||
secondary 35A77C
|
|
||||||
onSecondary FFFBF0
|
|
||||||
secondaryContainer E0DCC7
|
|
||||||
onSecondaryContainer 8DA101
|
|
||||||
tertiary 8DA101
|
|
||||||
onTertiary FFFBF0
|
|
||||||
tertiaryContainer E0DCC7
|
|
||||||
onTertiaryContainer 5C6A72
|
|
||||||
error F85552
|
|
||||||
onError FFFBF0
|
|
||||||
errorContainer E6E2CC
|
|
||||||
onErrorContainer F85552
|
|
||||||
primaryFixed 3A94C5
|
|
||||||
primaryFixedDim 5FAFD7
|
|
||||||
onPrimaryFixed FFFBF0
|
|
||||||
onPrimaryFixedVariant E0DCC7
|
|
||||||
secondaryFixed 35A77C
|
|
||||||
secondaryFixedDim 5FC198
|
|
||||||
onSecondaryFixed FFFBF0
|
|
||||||
onSecondaryFixedVariant E0DCC7
|
|
||||||
tertiaryFixed 8DA101
|
|
||||||
tertiaryFixedDim A7C080
|
|
||||||
onTertiaryFixed FFFBF0
|
|
||||||
onTertiaryFixedVariant E0DCC7
|
|
||||||
term0 5C6A72
|
|
||||||
term1 F85552
|
|
||||||
term2 8DA101
|
|
||||||
term3 DFA000
|
|
||||||
term4 3A94C5
|
|
||||||
term5 DF69BA
|
|
||||||
term6 35A77C
|
|
||||||
term7 5C6A72
|
|
||||||
term8 939F91
|
|
||||||
term9 F85552
|
|
||||||
term10 8DA101
|
|
||||||
term11 DFA000
|
|
||||||
term12 3A94C5
|
|
||||||
term13 DF69BA
|
|
||||||
term14 35A77C
|
|
||||||
term15 5C6A72
|
|
||||||
rosewater 5C6A72
|
|
||||||
flamingo DF69BA
|
|
||||||
pink DF69BA
|
|
||||||
mauve DF69BA
|
|
||||||
red F85552
|
|
||||||
maroon F85552
|
|
||||||
peach E66868
|
|
||||||
yellow DFA000
|
|
||||||
green 8DA101
|
|
||||||
teal 35A77C
|
|
||||||
sky 3A94C5
|
|
||||||
sapphire 3A94C5
|
|
||||||
blue 3A94C5
|
|
||||||
lavender 3A94C5
|
|
||||||
klink 3A94C5
|
|
||||||
klinkSelection 3A94C5
|
|
||||||
kvisited 35A77C
|
|
||||||
kvisitedSelection 35A77C
|
|
||||||
knegative F85552
|
|
||||||
knegativeSelection F85552
|
|
||||||
kneutral DFA000
|
|
||||||
kneutralSelection DFA000
|
|
||||||
kpositive 8DA101
|
|
||||||
kpositiveSelection 8DA101
|
|
||||||
text 5C6A72
|
|
||||||
subtext1 6F7C84
|
|
||||||
subtext0 939F91
|
|
||||||
overlay2 A6B0A0
|
|
||||||
overlay1 B9C0B0
|
|
||||||
overlay0 CCD3C2
|
|
||||||
surface2 EAE4CA
|
|
||||||
surface1 F3EAD3
|
|
||||||
surface0 FDF6E3
|
|
||||||
base FDF6E3
|
|
||||||
mantle FFFBF0
|
|
||||||
crust FFFEF9
|
|
||||||
success 8DA101
|
|
||||||
onSuccess FFFBF0
|
|
||||||
successContainer E0DCC7
|
|
||||||
onSuccessContainer 5C6A72
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 7FBBB3
|
|
||||||
secondary_paletteKeyColor 83C092
|
|
||||||
tertiary_paletteKeyColor A7C080
|
|
||||||
neutral_paletteKeyColor 2E383C
|
|
||||||
neutral_variant_paletteKeyColor 374145
|
|
||||||
background 323C41
|
|
||||||
onBackground D3C6AA
|
|
||||||
surface 3A454A
|
|
||||||
surfaceDim 282F34
|
|
||||||
surfaceBright 4D585D
|
|
||||||
surfaceContainerLowest 232A2E
|
|
||||||
surfaceContainerLow 414D54
|
|
||||||
surfaceContainer 434E53
|
|
||||||
surfaceContainerHigh 4D585D
|
|
||||||
surfaceContainerHighest 525C61
|
|
||||||
onSurface D3C6AA
|
|
||||||
surfaceVariant 434E53
|
|
||||||
onSurfaceVariant 9DA9A0
|
|
||||||
inverseSurface D3C6AA
|
|
||||||
inverseOnSurface 323C41
|
|
||||||
outline 859289
|
|
||||||
outlineVariant 4D585D
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 7FBBB3
|
|
||||||
primary 7FBBB3
|
|
||||||
onPrimary 323C41
|
|
||||||
primaryContainer 4D585D
|
|
||||||
onPrimaryContainer A7C080
|
|
||||||
inversePrimary 5A9A8F
|
|
||||||
secondary 83C092
|
|
||||||
onSecondary 323C41
|
|
||||||
secondaryContainer 4D585D
|
|
||||||
onSecondaryContainer A7C080
|
|
||||||
tertiary A7C080
|
|
||||||
onTertiary 323C41
|
|
||||||
tertiaryContainer 4D585D
|
|
||||||
onTertiaryContainer D3C6AA
|
|
||||||
error E67E80
|
|
||||||
onError 323C41
|
|
||||||
errorContainer 4C3743
|
|
||||||
onErrorContainer E67E80
|
|
||||||
primaryFixed 7FBBB3
|
|
||||||
primaryFixedDim 5A9A8F
|
|
||||||
onPrimaryFixed 323C41
|
|
||||||
onPrimaryFixedVariant 374145
|
|
||||||
secondaryFixed 83C092
|
|
||||||
secondaryFixedDim 5F8C6F
|
|
||||||
onSecondaryFixed 323C41
|
|
||||||
onSecondaryFixedVariant 374145
|
|
||||||
tertiaryFixed A7C080
|
|
||||||
tertiaryFixedDim 7F9D5F
|
|
||||||
onTertiaryFixed 323C41
|
|
||||||
onTertiaryFixedVariant 374145
|
|
||||||
term0 323C41
|
|
||||||
term1 E67E80
|
|
||||||
term2 A7C080
|
|
||||||
term3 DBBC7F
|
|
||||||
term4 7FBBB3
|
|
||||||
term5 D699B6
|
|
||||||
term6 83C092
|
|
||||||
term7 D3C6AA
|
|
||||||
term8 859289
|
|
||||||
term9 E67E80
|
|
||||||
term10 A7C080
|
|
||||||
term11 DBBC7F
|
|
||||||
term12 7FBBB3
|
|
||||||
term13 D699B6
|
|
||||||
term14 83C092
|
|
||||||
term15 D3C6AA
|
|
||||||
rosewater D3C6AA
|
|
||||||
flamingo D699B6
|
|
||||||
pink D699B6
|
|
||||||
mauve D699B6
|
|
||||||
red E67E80
|
|
||||||
maroon E67E80
|
|
||||||
peach E69875
|
|
||||||
yellow DBBC7F
|
|
||||||
green A7C080
|
|
||||||
teal 83C092
|
|
||||||
sky 7FBBB3
|
|
||||||
sapphire 7FBBB3
|
|
||||||
blue 7FBBB3
|
|
||||||
lavender 7FBBB3
|
|
||||||
klink 7FBBB3
|
|
||||||
klinkSelection 7FBBB3
|
|
||||||
kvisited 83C092
|
|
||||||
kvisitedSelection 83C092
|
|
||||||
knegative E67E80
|
|
||||||
knegativeSelection E67E80
|
|
||||||
kneutral DBBC7F
|
|
||||||
kneutralSelection DBBC7F
|
|
||||||
kpositive A7C080
|
|
||||||
kpositiveSelection A7C080
|
|
||||||
text D3C6AA
|
|
||||||
subtext1 9DA9A0
|
|
||||||
subtext0 859289
|
|
||||||
overlay2 7A8478
|
|
||||||
overlay1 6F7A6F
|
|
||||||
overlay0 5F6A5F
|
|
||||||
surface2 434E53
|
|
||||||
surface1 3A454A
|
|
||||||
surface0 323C41
|
|
||||||
base 323C41
|
|
||||||
mantle 282F34
|
|
||||||
crust 232A2E
|
|
||||||
success A7C080
|
|
||||||
onSuccess 323C41
|
|
||||||
successContainer 4D585D
|
|
||||||
onSuccessContainer D3C6AA
|
|
||||||
@@ -82,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 88C0D0
|
|
||||||
secondary_paletteKeyColor 81A1C1
|
|
||||||
tertiary_paletteKeyColor 5E81AC
|
|
||||||
neutral_paletteKeyColor 3B4252
|
|
||||||
neutral_variant_paletteKeyColor 434C5E
|
|
||||||
background 2E3440
|
|
||||||
onBackground ECEFF4
|
|
||||||
surface 3B4252
|
|
||||||
surfaceDim 242933
|
|
||||||
surfaceBright 4C566A
|
|
||||||
surfaceContainerLowest 1F232C
|
|
||||||
surfaceContainerLow 424A5E
|
|
||||||
surfaceContainer 434C5E
|
|
||||||
surfaceContainerHigh 4C566A
|
|
||||||
surfaceContainerHighest 55606E
|
|
||||||
onSurface ECEFF4
|
|
||||||
surfaceVariant 434C5E
|
|
||||||
onSurfaceVariant D8DEE9
|
|
||||||
inverseSurface ECEFF4
|
|
||||||
inverseOnSurface 2E3440
|
|
||||||
outline 616E88
|
|
||||||
outlineVariant 4C566A
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 88C0D0
|
|
||||||
primary 88C0D0
|
|
||||||
onPrimary 2E3440
|
|
||||||
primaryContainer 4C566A
|
|
||||||
onPrimaryContainer 88C0D0
|
|
||||||
inversePrimary 6FA3B3
|
|
||||||
secondary 81A1C1
|
|
||||||
onSecondary 2E3440
|
|
||||||
secondaryContainer 4C566A
|
|
||||||
onSecondaryContainer 81A1C1
|
|
||||||
tertiary 5E81AC
|
|
||||||
onTertiary 2E3440
|
|
||||||
tertiaryContainer 4C566A
|
|
||||||
onTertiaryContainer 5E81AC
|
|
||||||
error BF616A
|
|
||||||
onError 2E3440
|
|
||||||
errorContainer 4C3743
|
|
||||||
onErrorContainer BF616A
|
|
||||||
primaryFixed 88C0D0
|
|
||||||
primaryFixedDim 6FA3B3
|
|
||||||
onPrimaryFixed 2E3440
|
|
||||||
onPrimaryFixedVariant 434C5E
|
|
||||||
secondaryFixed 81A1C1
|
|
||||||
secondaryFixedDim 6A84A4
|
|
||||||
onSecondaryFixed 2E3440
|
|
||||||
onSecondaryFixedVariant 434C5E
|
|
||||||
tertiaryFixed 5E81AC
|
|
||||||
tertiaryFixedDim 4A6A8F
|
|
||||||
onTertiaryFixed 2E3440
|
|
||||||
onTertiaryFixedVariant 434C5E
|
|
||||||
term0 3B4252
|
|
||||||
term1 BF616A
|
|
||||||
term2 A3BE8C
|
|
||||||
term3 EBCB8B
|
|
||||||
term4 81A1C1
|
|
||||||
term5 B48EAD
|
|
||||||
term6 88C0D0
|
|
||||||
term7 E5E9F0
|
|
||||||
term8 4C566A
|
|
||||||
term9 BF616A
|
|
||||||
term10 A3BE8C
|
|
||||||
term11 EBCB8B
|
|
||||||
term12 81A1C1
|
|
||||||
term13 B48EAD
|
|
||||||
term14 8FBCBB
|
|
||||||
term15 ECEFF4
|
|
||||||
rosewater ECEFF4
|
|
||||||
flamingo B48EAD
|
|
||||||
pink B48EAD
|
|
||||||
mauve B48EAD
|
|
||||||
red BF616A
|
|
||||||
maroon BF616A
|
|
||||||
peach D08770
|
|
||||||
yellow EBCB8B
|
|
||||||
green A3BE8C
|
|
||||||
teal 8FBCBB
|
|
||||||
sky 88C0D0
|
|
||||||
sapphire 81A1C1
|
|
||||||
blue 5E81AC
|
|
||||||
lavender 5E81AC
|
|
||||||
klink 88C0D0
|
|
||||||
klinkSelection 88C0D0
|
|
||||||
kvisited 81A1C1
|
|
||||||
kvisitedSelection 81A1C1
|
|
||||||
knegative BF616A
|
|
||||||
knegativeSelection BF616A
|
|
||||||
kneutral EBCB8B
|
|
||||||
kneutralSelection EBCB8B
|
|
||||||
kpositive A3BE8C
|
|
||||||
kpositiveSelection A3BE8C
|
|
||||||
text ECEFF4
|
|
||||||
subtext1 D8DEE9
|
|
||||||
subtext0 616E88
|
|
||||||
overlay2 5A677E
|
|
||||||
overlay1 4F5B73
|
|
||||||
overlay0 434C5E
|
|
||||||
surface2 434C5E
|
|
||||||
surface1 3B4252
|
|
||||||
surface0 2E3440
|
|
||||||
base 2E3440
|
|
||||||
mantle 242933
|
|
||||||
crust 1F232C
|
|
||||||
success A3BE8C
|
|
||||||
onSuccess 2E3440
|
|
||||||
successContainer 4C566A
|
|
||||||
onSuccessContainer ECEFF4
|
|
||||||
@@ -82,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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,16 +82,6 @@ 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
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 268BD2
|
|
||||||
secondary_paletteKeyColor 2AA198
|
|
||||||
tertiary_paletteKeyColor 6C71C4
|
|
||||||
neutral_paletteKeyColor 002B36
|
|
||||||
neutral_variant_paletteKeyColor 073642
|
|
||||||
background 002B36
|
|
||||||
onBackground FDF6E3
|
|
||||||
surface 073642
|
|
||||||
surfaceDim 001F29
|
|
||||||
surfaceBright 0D4250
|
|
||||||
surfaceContainerLowest 00151D
|
|
||||||
surfaceContainerLow 0A404E
|
|
||||||
surfaceContainer 094B59
|
|
||||||
surfaceContainerHigh 0D4250
|
|
||||||
surfaceContainerHighest 11505E
|
|
||||||
onSurface FDF6E3
|
|
||||||
surfaceVariant 094B59
|
|
||||||
onSurfaceVariant 93A1A1
|
|
||||||
inverseSurface FDF6E3
|
|
||||||
inverseOnSurface 002B36
|
|
||||||
outline 586E75
|
|
||||||
outlineVariant 0D4250
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 268BD2
|
|
||||||
primary 268BD2
|
|
||||||
onPrimary 002B36
|
|
||||||
primaryContainer 0D4250
|
|
||||||
onPrimaryContainer 268BD2
|
|
||||||
inversePrimary 2075B2
|
|
||||||
secondary 2AA198
|
|
||||||
onSecondary 002B36
|
|
||||||
secondaryContainer 0D4250
|
|
||||||
onSecondaryContainer 2AA198
|
|
||||||
tertiary 6C71C4
|
|
||||||
onTertiary 002B36
|
|
||||||
tertiaryContainer 0D4250
|
|
||||||
onTertiaryContainer 6C71C4
|
|
||||||
error DC322F
|
|
||||||
onError 002B36
|
|
||||||
errorContainer 4C3743
|
|
||||||
onErrorContainer DC322F
|
|
||||||
primaryFixed 268BD2
|
|
||||||
primaryFixedDim 2075B2
|
|
||||||
onPrimaryFixed 002B36
|
|
||||||
onPrimaryFixedVariant 094B59
|
|
||||||
secondaryFixed 2AA198
|
|
||||||
secondaryFixedDim 228178
|
|
||||||
onSecondaryFixed 002B36
|
|
||||||
onSecondaryFixedVariant 094B59
|
|
||||||
tertiaryFixed 6C71C4
|
|
||||||
tertiaryFixedDim 5C61A4
|
|
||||||
onTertiaryFixed 002B36
|
|
||||||
onTertiaryFixedVariant 094B59
|
|
||||||
term0 002B36
|
|
||||||
term1 DC322F
|
|
||||||
term2 859900
|
|
||||||
term3 B58900
|
|
||||||
term4 268BD2
|
|
||||||
term5 D33682
|
|
||||||
term6 2AA198
|
|
||||||
term7 EEE8D5
|
|
||||||
term8 586E75
|
|
||||||
term9 CB4B16
|
|
||||||
term10 859900
|
|
||||||
term11 B58900
|
|
||||||
term12 268BD2
|
|
||||||
term13 6C71C4
|
|
||||||
term14 2AA198
|
|
||||||
term15 FDF6E3
|
|
||||||
rosewater FDF6E3
|
|
||||||
flamingo EEE8D5
|
|
||||||
pink D33682
|
|
||||||
mauve 6C71C4
|
|
||||||
red DC322F
|
|
||||||
maroon CB4B16
|
|
||||||
peach CB4B16
|
|
||||||
yellow B58900
|
|
||||||
green 859900
|
|
||||||
teal 2AA198
|
|
||||||
sky 2AA198
|
|
||||||
sapphire 268BD2
|
|
||||||
blue 268BD2
|
|
||||||
lavender 6C71C4
|
|
||||||
klink 268BD2
|
|
||||||
klinkSelection 268BD2
|
|
||||||
kvisited 6C71C4
|
|
||||||
kvisitedSelection 6C71C4
|
|
||||||
knegative DC322F
|
|
||||||
knegativeSelection DC322F
|
|
||||||
kneutral B58900
|
|
||||||
kneutralSelection B58900
|
|
||||||
kpositive 859900
|
|
||||||
kpositiveSelection 859900
|
|
||||||
text FDF6E3
|
|
||||||
subtext1 93A1A1
|
|
||||||
subtext0 839496
|
|
||||||
overlay2 657B83
|
|
||||||
overlay1 586E75
|
|
||||||
overlay0 073642
|
|
||||||
surface2 094B59
|
|
||||||
surface1 073642
|
|
||||||
surface0 002B36
|
|
||||||
base 002B36
|
|
||||||
mantle 001F29
|
|
||||||
crust 00151D
|
|
||||||
success 859900
|
|
||||||
onSuccess 002B36
|
|
||||||
successContainer 0D4250
|
|
||||||
onSuccessContainer FDF6E3
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
primary_paletteKeyColor 7AA2F7
|
|
||||||
secondary_paletteKeyColor 9ECE6A
|
|
||||||
tertiary_paletteKeyColor BB9AF7
|
|
||||||
neutral_paletteKeyColor 1A1B26
|
|
||||||
neutral_variant_paletteKeyColor 292E42
|
|
||||||
background 1A1B26
|
|
||||||
onBackground C0CAF5
|
|
||||||
surface 24283B
|
|
||||||
surfaceDim 16161E
|
|
||||||
surfaceBright 3B4261
|
|
||||||
surfaceContainerLowest 0F0F14
|
|
||||||
surfaceContainerLow 2B3048
|
|
||||||
surfaceContainer 2A2F41
|
|
||||||
surfaceContainerHigh 3B4261
|
|
||||||
surfaceContainerHighest 414868
|
|
||||||
onSurface C0CAF5
|
|
||||||
surfaceVariant 2A2F41
|
|
||||||
onSurfaceVariant A9B1D6
|
|
||||||
inverseSurface C0CAF5
|
|
||||||
inverseOnSurface 1A1B26
|
|
||||||
outline 565F89
|
|
||||||
outlineVariant 3B4261
|
|
||||||
shadow 000000
|
|
||||||
scrim 000000
|
|
||||||
surfaceTint 7AA2F7
|
|
||||||
primary 7AA2F7
|
|
||||||
onPrimary 1A1B26
|
|
||||||
primaryContainer 3B4261
|
|
||||||
onPrimaryContainer 7AA2F7
|
|
||||||
inversePrimary 5A7FD7
|
|
||||||
secondary 9ECE6A
|
|
||||||
onSecondary 1A1B26
|
|
||||||
secondaryContainer 3B4261
|
|
||||||
onSecondaryContainer 9ECE6A
|
|
||||||
tertiary BB9AF7
|
|
||||||
onTertiary 1A1B26
|
|
||||||
tertiaryContainer 3B4261
|
|
||||||
onTertiaryContainer BB9AF7
|
|
||||||
error F7768E
|
|
||||||
onError 1A1B26
|
|
||||||
errorContainer 4C3743
|
|
||||||
onErrorContainer F7768E
|
|
||||||
primaryFixed 7AA2F7
|
|
||||||
primaryFixedDim 5A7FD7
|
|
||||||
onPrimaryFixed 1A1B26
|
|
||||||
onPrimaryFixedVariant 2A2F41
|
|
||||||
secondaryFixed 9ECE6A
|
|
||||||
secondaryFixedDim 7EAE4A
|
|
||||||
onSecondaryFixed 1A1B26
|
|
||||||
onSecondaryFixedVariant 2A2F41
|
|
||||||
tertiaryFixed BB9AF7
|
|
||||||
tertiaryFixedDim 9B7AD7
|
|
||||||
onTertiaryFixed 1A1B26
|
|
||||||
onTertiaryFixedVariant 2A2F41
|
|
||||||
term0 1A1B26
|
|
||||||
term1 F7768E
|
|
||||||
term2 9ECE6A
|
|
||||||
term3 E0AF68
|
|
||||||
term4 7AA2F7
|
|
||||||
term5 BB9AF7
|
|
||||||
term6 7DCFFF
|
|
||||||
term7 C0CAF5
|
|
||||||
term8 565F89
|
|
||||||
term9 F7768E
|
|
||||||
term10 9ECE6A
|
|
||||||
term11 E0AF68
|
|
||||||
term12 7AA2F7
|
|
||||||
term13 BB9AF7
|
|
||||||
term14 7DCFFF
|
|
||||||
term15 C0CAF5
|
|
||||||
rosewater C0CAF5
|
|
||||||
flamingo BB9AF7
|
|
||||||
pink F7768E
|
|
||||||
mauve BB9AF7
|
|
||||||
red F7768E
|
|
||||||
maroon E0AF68
|
|
||||||
peach FF9E64
|
|
||||||
yellow E0AF68
|
|
||||||
green 9ECE6A
|
|
||||||
teal 1ABC9C
|
|
||||||
sky 7DCFFF
|
|
||||||
sapphire 2AC3DE
|
|
||||||
blue 7AA2F7
|
|
||||||
lavender 7DCFFF
|
|
||||||
klink 7AA2F7
|
|
||||||
klinkSelection 7AA2F7
|
|
||||||
kvisited BB9AF7
|
|
||||||
kvisitedSelection BB9AF7
|
|
||||||
knegative F7768E
|
|
||||||
knegativeSelection F7768E
|
|
||||||
kneutral E0AF68
|
|
||||||
kneutralSelection E0AF68
|
|
||||||
kpositive 9ECE6A
|
|
||||||
kpositiveSelection 9ECE6A
|
|
||||||
text C0CAF5
|
|
||||||
subtext1 A9B1D6
|
|
||||||
subtext0 9AA5CE
|
|
||||||
overlay2 787C99
|
|
||||||
overlay1 696D85
|
|
||||||
overlay0 565F89
|
|
||||||
surface2 2A2F41
|
|
||||||
surface1 24283B
|
|
||||||
surface0 1A1B26
|
|
||||||
base 1A1B26
|
|
||||||
mantle 16161E
|
|
||||||
crust 0F0F14
|
|
||||||
success 9ECE6A
|
|
||||||
onSuccess 1A1B26
|
|
||||||
successContainer 3B4261
|
|
||||||
onSuccessContainer C0CAF5
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
# Cava Audio Visualizer Configuration Template
|
|
||||||
# Optimized for smooth and responsive visualization
|
|
||||||
|
|
||||||
[general]
|
|
||||||
# Framerate (1-144) - higher = smoother but more CPU intensive
|
|
||||||
framerate = 60
|
|
||||||
|
|
||||||
[input]
|
|
||||||
# Audio input method: pulse, alsa, fifo, or portaudio
|
|
||||||
method = pulse
|
|
||||||
# Audio device (leave as default for auto-detection)
|
|
||||||
source = auto
|
|
||||||
|
|
||||||
[output]
|
|
||||||
# Output method: ncurses, terminal, raw, or circle
|
|
||||||
method = ncurses
|
|
||||||
# Terminal color scheme
|
|
||||||
style = stereo
|
|
||||||
|
|
||||||
[color]
|
|
||||||
# Color gradient for bars using template variables
|
|
||||||
gradient = 1
|
|
||||||
gradient_count = 8
|
|
||||||
gradient_color_1 = '{{ $green }}'
|
|
||||||
gradient_color_2 = '{{ $teal }}'
|
|
||||||
gradient_color_3 = '{{ $sky }}'
|
|
||||||
gradient_color_4 = '{{ $sapphire }}'
|
|
||||||
gradient_color_5 = '{{ $blue }}'
|
|
||||||
gradient_color_6 = '{{ $lavender }}'
|
|
||||||
gradient_color_7 = '{{ $mauve }}'
|
|
||||||
gradient_color_8 = '{{ $maroon }}'
|
|
||||||
|
|
||||||
[smoothing]
|
|
||||||
# Noise reduction (0-100) - higher = smoother but less responsive
|
|
||||||
# 77 is default, 85 provides good balance for smooth visualization
|
|
||||||
noise_reduction = 85
|
|
||||||
|
|
||||||
# Monstercat smoothing (0 or 1) - adds smoothing between adjacent bars
|
|
||||||
monstercat = 1
|
|
||||||
|
|
||||||
# Wave effect (0 or 1) - creates wave-like motion across bars
|
|
||||||
waves = 0
|
|
||||||
|
|
||||||
# Gravity (0-200) - controls how fast bars fall
|
|
||||||
# 100 = normal gravity, 150 = faster fall, 50 = slower fall
|
|
||||||
gravity = 120
|
|
||||||
|
|
||||||
[eq]
|
|
||||||
# Equalizer settings for frequency response
|
|
||||||
# Lower frequencies tend to be louder, so reduce them slightly
|
|
||||||
1 = 0.8
|
|
||||||
2 = 0.9
|
|
||||||
3 = 1.0
|
|
||||||
4 = 1.1
|
|
||||||
5 = 1.2
|
|
||||||
@@ -15,10 +15,8 @@
|
|||||||
@import url("https://refact0r.github.io/midnight-discord/build/midnight.css");
|
@import url("https://refact0r.github.io/midnight-discord/build/midnight.css");
|
||||||
|
|
||||||
body {
|
body {
|
||||||
/* font options */
|
/* font, change to '' for default discord font */
|
||||||
--font: "figtree"; /* change to '' for default discord font */
|
--font: "figtree";
|
||||||
--code-font: "JetBrainsMono NF"; /* change to '' for default discord font */
|
|
||||||
font-weight: 400; /* normal text font weight. DOES NOT AFFECT BOLD TEXT */
|
|
||||||
|
|
||||||
/* sizes */
|
/* sizes */
|
||||||
--gap: 12px; /* spacing between panels */
|
--gap: 12px; /* spacing between panels */
|
||||||
@@ -29,14 +27,13 @@ body {
|
|||||||
--animations: on; /* turn off to disable all midnight animations/transitions */
|
--animations: on; /* turn off to disable all midnight animations/transitions */
|
||||||
--list-item-transition: 0.2s ease; /* transition for list items */
|
--list-item-transition: 0.2s ease; /* transition for list items */
|
||||||
--dms-icon-svg-transition: 0.4s ease; /* transition for the dms icon */
|
--dms-icon-svg-transition: 0.4s ease; /* transition for the dms icon */
|
||||||
--border-hover-transition: 0.2s ease; /* transition for borders when hovered */
|
|
||||||
|
|
||||||
/* top bar options */
|
/* top bar options */
|
||||||
--top-bar-height: var(
|
--top-bar-height: var(
|
||||||
--gap
|
--gap
|
||||||
); /* height of the titlebar/top bar (discord default is 36px, 24px recommended if moving/hiding top bar buttons) */
|
); /* height of the titlebar/top bar (discord default is 36px, 24px recommended if moving/hiding top bar buttons) */
|
||||||
--top-bar-button-position: titlebar; /* off: default position, hide: hide inbox/support buttons completely, serverlist: move inbox button to server list, titlebar: move inbox button to titlebar (will hide title) */
|
--top-bar-button-position: hide; /* off: default position, hide: hide inbox/support buttons completely, serverlist: move inbox button to server list, titlebar: move inbox button to titlebar (will hide title) */
|
||||||
--top-bar-title-position: off; /* off: default centered position, hide: hide title completely, left: left align title (like old discord) */
|
--top-bar-title-position: hide; /* off: default centered position, hide: hide title completely, left: left align title (like old discord) */
|
||||||
--subtle-top-bar-title: off; /* off: default, on: hide the icon and use subtle text color (like old discord) */
|
--subtle-top-bar-title: off; /* off: default, on: hide the icon and use subtle text color (like old discord) */
|
||||||
|
|
||||||
/* window controls */
|
/* window controls */
|
||||||
@@ -45,9 +42,9 @@ body {
|
|||||||
|
|
||||||
/* dms button icon options */
|
/* dms button icon options */
|
||||||
--custom-dms-icon: custom; /* off: use default discord icon, hide: remove icon entirely, custom: use custom icon */
|
--custom-dms-icon: custom; /* off: use default discord icon, hide: remove icon entirely, custom: use custom icon */
|
||||||
--dms-icon-svg-url: url("https://refact0r.github.io/midnight-discord/assets/Font_Awesome_5_solid_moon.svg"); /* icon svg url. MUST BE A SVG. */
|
--dms-icon-svg-url: url("https://upload.wikimedia.org/wikipedia/commons/c/c4/Font_Awesome_5_solid_moon.svg"); /* icon svg url. MUST BE A SVG. */
|
||||||
--dms-icon-svg-size: 90%; /* size of the svg (css mask-size) */
|
--dms-icon-svg-size: 90%; /* size of the svg (css mask-size) */
|
||||||
--dms-icon-color-before: var(--icon-subtle); /* normal icon color */
|
--dms-icon-color-before: var(--icon-secondary); /* normal icon color */
|
||||||
--dms-icon-color-after: var(--white); /* icon color when button is hovered/selected */
|
--dms-icon-color-after: var(--white); /* icon color when button is hovered/selected */
|
||||||
|
|
||||||
/* dms button background options */
|
/* dms button background options */
|
||||||
@@ -74,11 +71,12 @@ body {
|
|||||||
--bg-floating: #{c.$surface}; /* you can set this to a more opaque color if floating panels look too transparent */
|
--bg-floating: #{c.$surface}; /* you can set this to a more opaque color if floating panels look too transparent */
|
||||||
|
|
||||||
/* chatbar options */
|
/* chatbar options */
|
||||||
--custom-chatbar: off; /* off: default chatbar, separated: chatbar separated from chat */
|
--custom-chatbar: aligned; /* off: default chatbar, aligned: chatbar aligned with the user panel, separated: chatbar separated from chat */
|
||||||
--chatbar-height: 47px; /* height of the chatbar (56px by default, 47px to align with user panel, 56px recommended for separated) */
|
--chatbar-height: 47px; /* height of the chatbar (52px by default, 47px recommended for aligned, 56px recommended for separated) */
|
||||||
|
--chatbar-padding: 8px; /* padding of the chatbar. only applies in aligned mode. */
|
||||||
|
|
||||||
/* other options */
|
/* other options */
|
||||||
--small-user-panel: off; /* off: default user panel, on: smaller user panel like in old discord */
|
--small-user-panel: off; /* turn on to make the user panel smaller like in old discord */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* color options */
|
/* color options */
|
||||||
|
|||||||
@@ -15,7 +15,3 @@
|
|||||||
@define-color sidebar_fg_color @window_fg_color;
|
@define-color sidebar_fg_color @window_fg_color;
|
||||||
@define-color sidebar_border_color @window_bg_color;
|
@define-color sidebar_border_color @window_bg_color;
|
||||||
@define-color sidebar_backdrop_color @window_bg_color;
|
@define-color sidebar_backdrop_color @window_bg_color;
|
||||||
@define-color theme_selected_bg_color alpha(@accent_color, 0.15);
|
|
||||||
@define-color theme_selected_fg_color @primary;
|
|
||||||
|
|
||||||
@import "thunar.css";
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
# 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 }}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# 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,162 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://github.com/longbridge/gpui-component/raw/refs/heads/main/.theme-schema.json",
|
|
||||||
"name": "Caelestia",
|
|
||||||
"author": "Unrectified",
|
|
||||||
"url": "https://github.com/caelestia-dots/cli",
|
|
||||||
"themes": [
|
|
||||||
{
|
|
||||||
"name": "Caelestia",
|
|
||||||
"mode": "{{ $mode }}",
|
|
||||||
"colors": {
|
|
||||||
"accent.background": "{{ $surfaceContainerHigh }}",
|
|
||||||
"accent.foreground": "{{ $onSurface }}",
|
|
||||||
"background": "{{ $background }}",
|
|
||||||
"border": "{{ $outlineVariant }}",
|
|
||||||
"danger.background": "{{ $error }}",
|
|
||||||
"foreground": "{{ $onBackground }}",
|
|
||||||
"input.border": "{{ $outline }}",
|
|
||||||
"link.active.foreground": "{{ $primary }}",
|
|
||||||
"link.foreground": "{{ $primary }}",
|
|
||||||
"link.hover.foreground": "{{ $primaryFixed }}",
|
|
||||||
"list.active.background": "{{ $secondaryContainer }}",
|
|
||||||
"list.active.border": "{{ $secondary }}",
|
|
||||||
"list.even.background": "{{ $surfaceContainerLowest }}",
|
|
||||||
"muted.background": "{{ $surfaceVariant }}",
|
|
||||||
"muted.foreground": "{{ $onSurfaceVariant }}",
|
|
||||||
"panel.background": "{{ $surfaceContainer }}",
|
|
||||||
"popover.background": "{{ $surfaceContainerHigh }}",
|
|
||||||
"popover.foreground": "{{ $onSurface }}",
|
|
||||||
"primary.active.background": "{{ $primaryFixedDim }}",
|
|
||||||
"primary.background": "{{ $primary }}",
|
|
||||||
"primary.foreground": "{{ $onPrimary }}",
|
|
||||||
"primary.hover.background": "{{ $primaryFixed }}",
|
|
||||||
"scrollbar.background": "{{ $surface }}",
|
|
||||||
"scrollbar.thumb.background": "{{ $outline }}",
|
|
||||||
"secondary.background": "{{ $secondaryContainer }}",
|
|
||||||
"secondary.active.background": "{{ $secondaryFixedDim }}",
|
|
||||||
"secondary.foreground": "{{ $onSecondary }}",
|
|
||||||
"secondary.hover.background": "{{ $secondaryFixed }}",
|
|
||||||
"tab.active.background": "{{ $surface }}",
|
|
||||||
"tab.active.foreground": "{{ $onSurface }}",
|
|
||||||
"tab.background": "{{ $surfaceContainerLowest }}",
|
|
||||||
"tab.foreground": "{{ $onSurfaceVariant }}",
|
|
||||||
"tab_bar.background": "{{ $surface }}",
|
|
||||||
"table.background": "{{ $surfaceContainer }}",
|
|
||||||
"table.head.foreground": "{{ $onSurfaceVariant }}",
|
|
||||||
"table.row.border": "{{ $outlineVariant }}",
|
|
||||||
"title_bar.background": "{{ $surfaceDim }}",
|
|
||||||
"ring": "{{ $primary }}",
|
|
||||||
"base.red": "{{ $red }}",
|
|
||||||
"base.red.light": "{{ $peach }}",
|
|
||||||
"base.green": "{{ $green }}",
|
|
||||||
"base.green.light": "{{ $teal }}",
|
|
||||||
"base.blue": "{{ $blue }}",
|
|
||||||
"base.blue.light": "{{ $sky }}",
|
|
||||||
"base.cyan": "{{ $teal }}",
|
|
||||||
"base.cyan.light": "{{ $sky }}",
|
|
||||||
"base.magenta": "{{ $mauve }}",
|
|
||||||
"base.magenta.light": "{{ $pink }}",
|
|
||||||
"base.yellow": "{{ $yellow }}",
|
|
||||||
"base.yellow.light": "{{ $peach }}"
|
|
||||||
},
|
|
||||||
"highlight": {
|
|
||||||
"editor.foreground": "{{ $onSurface }}",
|
|
||||||
"editor.background": "{{ $surface }}",
|
|
||||||
"editor.active_line.background": "{{ $surfaceContainerLow }}",
|
|
||||||
"editor.line_number": "{{ $onSurfaceVariant }}",
|
|
||||||
"editor.active_line_number": "{{ $onSurface }}",
|
|
||||||
"editor.invisible": "{{ $outlineVariant }}",
|
|
||||||
"conflict": "{{ $red }}",
|
|
||||||
"created": "{{ $green }}",
|
|
||||||
"deleted": "{{ $red }}",
|
|
||||||
"error": "{{ $error }}",
|
|
||||||
"hidden": "{{ $outline }}",
|
|
||||||
"hint": "{{ $success }}",
|
|
||||||
"ignored": "{{ $outline }}",
|
|
||||||
"info": "{{ $blue }}",
|
|
||||||
"modified": "{{ $yellow }}",
|
|
||||||
"predictive": "{{ $overlay1 }}",
|
|
||||||
"renamed": "{{ $green }}",
|
|
||||||
"success": "{{ $success }}",
|
|
||||||
"unreachable": "{{ $outlineVariant }}",
|
|
||||||
"warning": "{{ $yellow }}",
|
|
||||||
"syntax": {
|
|
||||||
"attribute": {
|
|
||||||
"color": "{{ $yellow }}"
|
|
||||||
},
|
|
||||||
"boolean": {
|
|
||||||
"color": "{{ $green }}"
|
|
||||||
},
|
|
||||||
"comment": {
|
|
||||||
"color": "{{ $subtext0 }}",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"comment.doc": {
|
|
||||||
"color": "{{ $subtext0 }}",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"constant": {
|
|
||||||
"color": "{{ $red }}"
|
|
||||||
},
|
|
||||||
"constructor": {
|
|
||||||
"color": "{{ $yellow }}"
|
|
||||||
},
|
|
||||||
"embedded": {
|
|
||||||
"color": "{{ $onSurface }}"
|
|
||||||
},
|
|
||||||
"function": {
|
|
||||||
"color": "{{ $green }}"
|
|
||||||
},
|
|
||||||
"keyword": {
|
|
||||||
"color": "{{ $mauve }}"
|
|
||||||
},
|
|
||||||
"link_text": {
|
|
||||||
"color": "{{ $sky }}",
|
|
||||||
"font_style": "normal"
|
|
||||||
},
|
|
||||||
"link_uri": {
|
|
||||||
"color": "{{ $klink }}",
|
|
||||||
"font_style": "italic"
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"color": "{{ $red }}"
|
|
||||||
},
|
|
||||||
"string": {
|
|
||||||
"color": "{{ $green }}"
|
|
||||||
},
|
|
||||||
"string.escape": {
|
|
||||||
"color": "{{ $green }}"
|
|
||||||
},
|
|
||||||
"string.regex": {
|
|
||||||
"color": "{{ $green }}"
|
|
||||||
},
|
|
||||||
"string.special": {
|
|
||||||
"color": "{{ $yellow }}"
|
|
||||||
},
|
|
||||||
"string.special.symbol": {
|
|
||||||
"color": "{{ $yellow }}"
|
|
||||||
},
|
|
||||||
"tag": {
|
|
||||||
"color": "{{ $yellow }}"
|
|
||||||
},
|
|
||||||
"text.literal": {
|
|
||||||
"color": "{{ $red }}"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"color": "{{ $sky }}",
|
|
||||||
"font_weight": 600
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"color": "{{ $yellow }}"
|
|
||||||
},
|
|
||||||
"property": {
|
|
||||||
"color": "{{ $onSurface }}"
|
|
||||||
},
|
|
||||||
"variable.special": {
|
|
||||||
"color": "{{ $red }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[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 }}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[Appearance]
|
||||||
|
color_scheme_path={{ $config }}/colors/caelestia.conf
|
||||||
|
custom_palette=true
|
||||||
|
icon_theme=Papirus-{{ $mode }}
|
||||||
|
standard_dialogs=default
|
||||||
|
style=Fusion
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
[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 }}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"theme": {
|
|
||||||
"colorScheme": "~/.config/qtengine/caelestia.colors",
|
|
||||||
"iconTheme": "Papirus-{{ $mode }}",
|
|
||||||
"style": "Darkly",
|
|
||||||
"font": {
|
|
||||||
"family": "Sans Serif",
|
|
||||||
"size": 12,
|
|
||||||
"weight": -1
|
|
||||||
},
|
|
||||||
"fontFixed": {
|
|
||||||
"family": "Monospace",
|
|
||||||
"size": 12,
|
|
||||||
"weight": -1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"misc": {
|
|
||||||
"menusHaveIcons": true,
|
|
||||||
"singleClickActivate": false,
|
|
||||||
"shortcutsForContextMenus": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
[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 }}
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
/* Thunar theme */
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Global Resets
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.thunar * {
|
|
||||||
outline: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Window & Background
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.thunar.background {
|
|
||||||
background: {{ $surface }};
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .titlebar {
|
|
||||||
background: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .titlebutton.close {
|
|
||||||
margin: 0 15px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Layout Containers
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
/* Paned separator between sidebar and main view */
|
|
||||||
.thunar paned > separator {
|
|
||||||
min-width: 4px;
|
|
||||||
margin-right: -7px;
|
|
||||||
margin-left: -7px;
|
|
||||||
background: none;
|
|
||||||
background-image: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Main file view frame */
|
|
||||||
.thunar .frame.standard-view {
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px 15px 0 0;
|
|
||||||
border-radius: 15px;
|
|
||||||
background-color: {{ $surfaceContainerLow }};
|
|
||||||
animation: fading 400ms ease forwards;
|
|
||||||
opacity: 0;
|
|
||||||
animation-delay: 250ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .frame.standard-view .view:not(.rubberband),
|
|
||||||
.thunar .frame.standard-view .view *:not(.rubberband) {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .frame.standard-view .view *:selected {
|
|
||||||
color: {{ $primary }};
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .rubberband {
|
|
||||||
background-color: alpha({{ $primary }}, 0.15);
|
|
||||||
border: 1px solid alpha({{ $primary }}, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Tabs */
|
|
||||||
.thunar header.top {
|
|
||||||
background: none;
|
|
||||||
padding: 0 10px 0 0;
|
|
||||||
margin: 3px 0 -3px -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar header.top tabs .reorderable-page {
|
|
||||||
margin: 0;
|
|
||||||
transition: all ease 300ms;
|
|
||||||
}
|
|
||||||
.thunar header.top tabs .reorderable-page + .reorderable-page {
|
|
||||||
margin: 0 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar header.top tabs .reorderable-page:hover {
|
|
||||||
background-color: alpha({{ $primary }}, 0.08);
|
|
||||||
|
|
||||||
}
|
|
||||||
.thunar header.top tabs .reorderable-page:checked {
|
|
||||||
color: {{ $primary }};
|
|
||||||
background-color: alpha({{ $primary }}, 0.15);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Sidebar Navigation
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.thunar .sidebar {
|
|
||||||
padding: 0 20px;
|
|
||||||
background: none;
|
|
||||||
animation: fading 600ms ease forwards;
|
|
||||||
animation-delay: 100ms;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .sidebar .view {
|
|
||||||
padding: 8px 4px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: none;
|
|
||||||
transition: all ease 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .sidebar .view:hover {
|
|
||||||
background: alpha({{ $onSurface }}, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .sidebar .view:selected {
|
|
||||||
background: alpha({{ $primary }}, 0.15);
|
|
||||||
color: {{ $primary }};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Path Bar & Location Buttons
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.thunar .path-bar-button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px 5px;
|
|
||||||
transition: all ease 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .location-button.toggle:checked,
|
|
||||||
.thunar .path-bar-button.toggle:checked {
|
|
||||||
padding: 8px 25px;
|
|
||||||
background: alpha({{ $primary }}, 0.15);
|
|
||||||
color: {{ $primary }};
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .location-button.path-bar-button:not(:checked) {
|
|
||||||
background-color: {{ $surfaceContainerLow }};
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .location-button.path-bar-button:not(:checked):hover {
|
|
||||||
background: alpha({{ $primary }}, 0.08);
|
|
||||||
color: alpha({{ $primary }}, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .location-button.toggle+.location-button.toggle:checked {
|
|
||||||
margin-left: 0px;
|
|
||||||
padding: 0 25px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .titlebar {
|
|
||||||
padding: 15px 0 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Buttons
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.thunar button.toggle:checked {
|
|
||||||
color: {{ $primary }};
|
|
||||||
}
|
|
||||||
|
|
||||||
.thunar .image-button {
|
|
||||||
padding: 8px;
|
|
||||||
margin: 0 0 0 8px;
|
|
||||||
transition: all ease 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Status Bar
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.thunar statusbar {
|
|
||||||
background-color: {{ $surfaceContainerLow }};
|
|
||||||
border-radius: 15px;
|
|
||||||
padding: 10px 10px;
|
|
||||||
margin: 15px 5px 15px -10px;
|
|
||||||
color: {{ $onSurface }};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Image preview
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
.thunar box.vertical .image {
|
|
||||||
margin: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* =============================================================================
|
|
||||||
Animation
|
|
||||||
============================================================================= */
|
|
||||||
|
|
||||||
@keyframes fading {
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
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 }}'
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
|
|
||||||
"name": "Caelestia",
|
|
||||||
"author": "Caelestia",
|
|
||||||
"themes": [
|
|
||||||
{
|
|
||||||
"name": "Caelestia",
|
|
||||||
"appearance": "{{ mode }}",
|
|
||||||
"style": {
|
|
||||||
"background": "#{{ surface.hex }}",
|
|
||||||
"border": "#{{ outlineVariant.hex }}40",
|
|
||||||
"border.variant": "#{{ outlineVariant.hex }}60",
|
|
||||||
"border.focused": "#{{ primary.hex }}",
|
|
||||||
"border.selected": "#{{ primary.hex }}80",
|
|
||||||
"border.transparent": "#00000000",
|
|
||||||
"border.disabled": "#{{ outlineVariant.hex }}30",
|
|
||||||
|
|
||||||
"elevated_surface.background": "#{{ surfaceContainerHigh.hex }}",
|
|
||||||
"surface.background": "#{{ surface.hex }}",
|
|
||||||
|
|
||||||
"element.background": "#{{ outlineVariant.hex }}40",
|
|
||||||
"element.hover": "#{{ outlineVariant.hex }}60",
|
|
||||||
"element.active": "#{{ primary.hex }}30",
|
|
||||||
"element.selected": "#{{ primary.hex }}20",
|
|
||||||
"element.disabled": "#{{ outlineVariant.hex }}20",
|
|
||||||
|
|
||||||
"drop_target.background": "#{{ primary.hex }}20",
|
|
||||||
|
|
||||||
"ghost_element.background": "#00000000",
|
|
||||||
"ghost_element.hover": "#{{ outlineVariant.hex }}40",
|
|
||||||
"ghost_element.active": "#{{ primary.hex }}30",
|
|
||||||
"ghost_element.selected": "#{{ primary.hex }}20",
|
|
||||||
"ghost_element.disabled": "#{{ outlineVariant.hex }}20",
|
|
||||||
|
|
||||||
"text": "#{{ onSurface.hex }}",
|
|
||||||
"text.muted": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"text.placeholder": "#{{ outline.hex }}",
|
|
||||||
"text.disabled": "#{{ outline.hex }}80",
|
|
||||||
"text.accent": "#{{ primary.hex }}",
|
|
||||||
|
|
||||||
"icon": "#{{ onSurface.hex }}",
|
|
||||||
"icon.muted": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"icon.disabled": "#{{ outlineVariant.hex }}60",
|
|
||||||
"icon.placeholder": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"icon.accent": "#{{ primary.hex }}",
|
|
||||||
|
|
||||||
"status_bar.background": "#{{ surface.hex }}",
|
|
||||||
"title_bar.background": "#{{ surface.hex }}",
|
|
||||||
"title_bar.inactive_background": "#{{ surface.hex }}",
|
|
||||||
"toolbar.background": "#{{ surface.hex }}",
|
|
||||||
"tab_bar.background": "#{{ surface.hex }}",
|
|
||||||
"tab.inactive_background": "#{{ surface.hex }}",
|
|
||||||
"tab.active_background": "#{{ surfaceContainerHigh.hex }}",
|
|
||||||
|
|
||||||
"search.match_background": "#{{ yellow.hex }}40",
|
|
||||||
|
|
||||||
"panel.background": "#{{ surface.hex }}",
|
|
||||||
"panel.focused_border": "#{{ primary.hex }}",
|
|
||||||
|
|
||||||
"pane.focused_border": "#{{ primary.hex }}",
|
|
||||||
|
|
||||||
"scrollbar.thumb.background": "#{{ outlineVariant.hex }}30",
|
|
||||||
"scrollbar.thumb.hover_background": "#{{ outlineVariant.hex }}60",
|
|
||||||
"scrollbar.thumb.border": "#{{ outlineVariant.hex }}20",
|
|
||||||
"scrollbar.track.background": "#00000000",
|
|
||||||
"scrollbar.track.border": "#00000000",
|
|
||||||
|
|
||||||
"editor.foreground": "#{{ onSurface.hex }}",
|
|
||||||
"editor.background": "#{{ surface.hex }}",
|
|
||||||
"editor.gutter.background": "#{{ surface.hex }}",
|
|
||||||
"editor.subheader.background": "#{{ surfaceContainer.hex }}",
|
|
||||||
"editor.active_line.background": "#{{ surfaceContainerHigh.hex }}60",
|
|
||||||
"editor.highlighted_line.background": "#{{ primary.hex }}15",
|
|
||||||
"editor.line_number": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"editor.active_line_number": "#{{ onSurface.hex }}",
|
|
||||||
"editor.invisible": "#{{ outlineVariant.hex }}40",
|
|
||||||
"editor.wrap_guide": "#{{ outlineVariant.hex }}30",
|
|
||||||
"editor.active_wrap_guide": "#{{ outlineVariant.hex }}60",
|
|
||||||
"editor.document_highlight.read_background": "#{{ primary.hex }}20",
|
|
||||||
"editor.document_highlight.write_background": "#{{ primary.hex }}30",
|
|
||||||
|
|
||||||
"terminal.background": "#{{ surface.hex }}",
|
|
||||||
"terminal.foreground": "#{{ onSurface.hex }}",
|
|
||||||
"terminal.bright_foreground": "#{{ onSurface.hex }}",
|
|
||||||
"terminal.dim_foreground": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"terminal.ansi.black": "#{{ surface.hex }}",
|
|
||||||
"terminal.ansi.bright_black": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"terminal.ansi.dim_black": "#{{ surface.hex }}80",
|
|
||||||
"terminal.ansi.red": "#{{ red.hex }}",
|
|
||||||
"terminal.ansi.bright_red": "#{{ maroon.hex }}",
|
|
||||||
"terminal.ansi.dim_red": "#{{ red.hex }}80",
|
|
||||||
"terminal.ansi.green": "#{{ green.hex }}",
|
|
||||||
"terminal.ansi.bright_green": "#{{ teal.hex }}",
|
|
||||||
"terminal.ansi.dim_green": "#{{ green.hex }}80",
|
|
||||||
"terminal.ansi.yellow": "#{{ yellow.hex }}",
|
|
||||||
"terminal.ansi.bright_yellow": "#{{ peach.hex }}",
|
|
||||||
"terminal.ansi.dim_yellow": "#{{ yellow.hex }}80",
|
|
||||||
"terminal.ansi.blue": "#{{ blue.hex }}",
|
|
||||||
"terminal.ansi.bright_blue": "#{{ sapphire.hex }}",
|
|
||||||
"terminal.ansi.dim_blue": "#{{ blue.hex }}80",
|
|
||||||
"terminal.ansi.magenta": "#{{ mauve.hex }}",
|
|
||||||
"terminal.ansi.bright_magenta": "#{{ pink.hex }}",
|
|
||||||
"terminal.ansi.dim_magenta": "#{{ mauve.hex }}80",
|
|
||||||
"terminal.ansi.cyan": "#{{ teal.hex }}",
|
|
||||||
"terminal.ansi.bright_cyan": "#{{ sky.hex }}",
|
|
||||||
"terminal.ansi.dim_cyan": "#{{ teal.hex }}80",
|
|
||||||
"terminal.ansi.white": "#{{ onSurface.hex }}",
|
|
||||||
"terminal.ansi.bright_white": "#{{ onSurface.hex }}",
|
|
||||||
"terminal.ansi.dim_white": "#{{ onSurface.hex }}80",
|
|
||||||
|
|
||||||
"link_text.hover": "#{{ primary.hex }}",
|
|
||||||
|
|
||||||
"conflict": "#{{ yellow.hex }}",
|
|
||||||
"conflict.background": "#{{ yellow.hex }}15",
|
|
||||||
"conflict.border": "#{{ yellow.hex }}",
|
|
||||||
|
|
||||||
"created": "#{{ green.hex }}",
|
|
||||||
"created.background": "#{{ green.hex }}15",
|
|
||||||
"created.border": "#{{ green.hex }}",
|
|
||||||
|
|
||||||
"deleted": "#{{ red.hex }}",
|
|
||||||
"deleted.background": "#{{ red.hex }}15",
|
|
||||||
"deleted.border": "#{{ red.hex }}",
|
|
||||||
|
|
||||||
"error": "#{{ error.hex }}",
|
|
||||||
"error.background": "#{{ error.hex }}15",
|
|
||||||
"error.border": "#{{ error.hex }}",
|
|
||||||
|
|
||||||
"hidden": "#{{ outline.hex }}",
|
|
||||||
"hidden.background": "#{{ outline.hex }}15",
|
|
||||||
"hidden.border": "#{{ outline.hex }}",
|
|
||||||
|
|
||||||
"hint": "#{{ success.hex }}",
|
|
||||||
"hint.background": "#{{ success.hex }}15",
|
|
||||||
"hint.border": "#{{ success.hex }}",
|
|
||||||
|
|
||||||
"ignored": "#{{ outline.hex }}",
|
|
||||||
"ignored.background": "#{{ outline.hex }}15",
|
|
||||||
"ignored.border": "#{{ outline.hex }}",
|
|
||||||
|
|
||||||
"info": "#{{ blue.hex }}",
|
|
||||||
"info.background": "#{{ blue.hex }}15",
|
|
||||||
"info.border": "#{{ blue.hex }}",
|
|
||||||
|
|
||||||
"modified": "#{{ peach.hex }}",
|
|
||||||
"modified.background": "#{{ peach.hex }}15",
|
|
||||||
"modified.border": "#{{ peach.hex }}",
|
|
||||||
|
|
||||||
"predictive": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"predictive.background": "#{{ onSurfaceVariant.hex }}15",
|
|
||||||
"predictive.border": "#{{ outlineVariant.hex }}40",
|
|
||||||
|
|
||||||
"renamed": "#{{ teal.hex }}",
|
|
||||||
"renamed.background": "#{{ teal.hex }}15",
|
|
||||||
"renamed.border": "#{{ teal.hex }}",
|
|
||||||
|
|
||||||
"success": "#{{ success.hex }}",
|
|
||||||
"success.background": "#{{ success.hex }}15",
|
|
||||||
"success.border": "#{{ success.hex }}",
|
|
||||||
|
|
||||||
"unreachable": "#{{ outline.hex }}",
|
|
||||||
"unreachable.background": "#{{ outline.hex }}15",
|
|
||||||
"unreachable.border": "#{{ outline.hex }}",
|
|
||||||
|
|
||||||
"warning": "#{{ yellow.hex }}",
|
|
||||||
"warning.background": "#{{ yellow.hex }}15",
|
|
||||||
"warning.border": "#{{ yellow.hex }}",
|
|
||||||
|
|
||||||
"players": [
|
|
||||||
{
|
|
||||||
"cursor": "#{{ onSurface.hex }}",
|
|
||||||
"selection": "#{{ onSurface.hex }}60",
|
|
||||||
"background": "#{{ primary.hex }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cursor": "#{{ teal.hex }}",
|
|
||||||
"selection": "#{{ teal.hex }}40",
|
|
||||||
"background": "#{{ teal.hex }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cursor": "#{{ pink.hex }}",
|
|
||||||
"selection": "#{{ pink.hex }}40",
|
|
||||||
"background": "#{{ pink.hex }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cursor": "#{{ yellow.hex }}",
|
|
||||||
"selection": "#{{ yellow.hex }}40",
|
|
||||||
"background": "#{{ yellow.hex }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cursor": "#{{ green.hex }}",
|
|
||||||
"selection": "#{{ green.hex }}40",
|
|
||||||
"background": "#{{ green.hex }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cursor": "#{{ red.hex }}",
|
|
||||||
"selection": "#{{ red.hex }}40",
|
|
||||||
"background": "#{{ red.hex }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cursor": "#{{ blue.hex }}",
|
|
||||||
"selection": "#{{ blue.hex }}40",
|
|
||||||
"background": "#{{ blue.hex }}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cursor": "#{{ maroon.hex }}",
|
|
||||||
"selection": "#{{ maroon.hex }}40",
|
|
||||||
"background": "#{{ maroon.hex }}"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"syntax": {
|
|
||||||
"attribute": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"boolean": {
|
|
||||||
"color": "#{{ peach.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"comment": {
|
|
||||||
"color": "#{{ subtext0.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"comment.doc": {
|
|
||||||
"color": "#{{ subtext0.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"constant": {
|
|
||||||
"color": "#{{ peach.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"constructor": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"embedded": {
|
|
||||||
"color": "#{{ onSurface.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"emphasis": {
|
|
||||||
"color": "#{{ red.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"emphasis.strong": {
|
|
||||||
"color": "#{{ red.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": 700
|
|
||||||
},
|
|
||||||
"enum": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"function": {
|
|
||||||
"color": "#{{ blue.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"function.builtin": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"function.definition": {
|
|
||||||
"color": "#{{ blue.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"function.method": {
|
|
||||||
"color": "#{{ blue.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"function.special.definition": {
|
|
||||||
"color": "#{{ blue.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"hint": {
|
|
||||||
"color": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"keyword": {
|
|
||||||
"color": "#{{ pink.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"label": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"link_text": {
|
|
||||||
"color": "#{{ blue.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"link_uri": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": "underline",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"number": {
|
|
||||||
"color": "#{{ peach.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"operator": {
|
|
||||||
"color": "#{{ sapphire.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"predictive": {
|
|
||||||
"color": "#{{ onSurfaceVariant.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"preproc": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"primary": {
|
|
||||||
"color": "#{{ onSurface.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"property": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"punctuation": {
|
|
||||||
"color": "#{{ subtext1.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"punctuation.bracket": {
|
|
||||||
"color": "#{{ subtext1.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"punctuation.delimiter": {
|
|
||||||
"color": "#{{ subtext1.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"punctuation.list_marker": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"punctuation.special": {
|
|
||||||
"color": "#{{ sapphire.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string": {
|
|
||||||
"color": "#{{ green.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string.escape": {
|
|
||||||
"color": "#{{ pink.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string.regex": {
|
|
||||||
"color": "#{{ sky.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string.special": {
|
|
||||||
"color": "#{{ green.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"string.special.symbol": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"tag": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"text.literal": {
|
|
||||||
"color": "#{{ green.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"color": "#{{ blue.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": 700
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"type.builtin": {
|
|
||||||
"color": "#{{ onSurface.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"type.interface": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"type.super": {
|
|
||||||
"color": "#{{ yellow.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"variable": {
|
|
||||||
"color": "#{{ onSurface.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"variable.member": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"variable.parameter": {
|
|
||||||
"color": "#{{ teal.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"variable.special": {
|
|
||||||
"color": "#{{ onSurface.hex }}",
|
|
||||||
"font_style": "italic",
|
|
||||||
"font_weight": null
|
|
||||||
},
|
|
||||||
"variant": {
|
|
||||||
"color": "#{{ peach.hex }}",
|
|
||||||
"font_style": null,
|
|
||||||
"font_weight": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
+137
-126
@@ -1,117 +1,199 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
|
||||||
|
|
||||||
from caelestia.subcommands import (
|
from caelestia.subcommands import (
|
||||||
clipboard,
|
clipboard,
|
||||||
emoji,
|
emoji,
|
||||||
install,
|
pip,
|
||||||
record,
|
record,
|
||||||
resizer,
|
|
||||||
scheme,
|
scheme,
|
||||||
screenshot,
|
screenshot,
|
||||||
shell,
|
shell,
|
||||||
toggle,
|
toggle,
|
||||||
update,
|
|
||||||
wallpaper,
|
wallpaper,
|
||||||
)
|
)
|
||||||
from caelestia.utils.dots.manifest import Manifest
|
|
||||||
from caelestia.utils.dots.packages import AUR_HELPERS
|
|
||||||
from caelestia.utils.dots.source import DotsSource
|
|
||||||
from caelestia.utils.io import warn
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> tuple[argparse.ArgumentParser, argparse.Namespace]:
|
def parse_args() -> (argparse.ArgumentParser, argparse.Namespace):
|
||||||
parser = argparse.ArgumentParser(prog="caelestia", description="Main control script for the Caelestia dotfiles")
|
parser = argparse.ArgumentParser(
|
||||||
parser.add_argument("-v", "--version", action="store_true", help="print the current version")
|
prog="caelestia", description="Main control script for the Caelestia dotfiles"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-v", "--version", action="store_true", help="print the current version"
|
||||||
|
)
|
||||||
|
|
||||||
# Add subcommand parsers
|
# Add subcommand parsers
|
||||||
command_parser = parser.add_subparsers(
|
command_parser = parser.add_subparsers(
|
||||||
title="subcommands", description="valid subcommands", metavar="COMMAND", help="the subcommand to run"
|
title="subcommands",
|
||||||
|
description="valid subcommands",
|
||||||
|
metavar="COMMAND",
|
||||||
|
help="the subcommand to run",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create parser for shell opts
|
# Create parser for shell opts
|
||||||
shell_parser = command_parser.add_parser("shell", help="start or message the shell")
|
shell_parser = command_parser.add_parser("shell", help="start or message the shell")
|
||||||
shell_parser.set_defaults(cls=shell.Command)
|
shell_parser.set_defaults(cls=shell.Command)
|
||||||
shell_parser.add_argument("message", nargs="*", help="a message to send to the shell")
|
shell_parser.add_argument(
|
||||||
shell_parser.add_argument("-d", "--daemon", action="store_true", help="start the shell detached")
|
"message", nargs="*", help="a message to send to the shell"
|
||||||
shell_parser.add_argument("-s", "--show", action="store_true", help="print all shell IPC commands")
|
)
|
||||||
shell_parser.add_argument("-l", "--log", 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")
|
"-d", "--daemon", action="store_true", help="start the shell detached"
|
||||||
shell_parser.add_argument("--log-rules", metavar="RULES", help="log rules to apply")
|
)
|
||||||
|
shell_parser.add_argument(
|
||||||
|
"-s", "--show", action="store_true", help="print all shell IPC commands"
|
||||||
|
)
|
||||||
|
shell_parser.add_argument(
|
||||||
|
"-l", "--log", action="store_true", help="print the shell log"
|
||||||
|
)
|
||||||
|
shell_parser.add_argument(
|
||||||
|
"--log-rules",
|
||||||
|
default="quickshell.dbus.properties.warning=false;quickshell.dbus.dbusmenu.warning=false;quickshell.service.notifications.warning=false;quickshell.service.sni.host.warning=false;qt.qpa.wayland.textinput.warning=false",
|
||||||
|
metavar="RULES",
|
||||||
|
help="log rules to apply",
|
||||||
|
)
|
||||||
|
|
||||||
# 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("workspace", help="the workspace to toggle")
|
toggle_parser.add_argument(
|
||||||
|
"workspace",
|
||||||
|
choices=["communication", "music", "sysmon", "specialws", "todo", "steam"],
|
||||||
|
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")
|
||||||
scheme_command_parser = scheme_parser.add_subparsers(title="subcommands")
|
scheme_command_parser = scheme_parser.add_subparsers(title="subcommands")
|
||||||
|
|
||||||
list_parser = scheme_command_parser.add_parser("list", help="list available schemes")
|
list_parser = scheme_command_parser.add_parser(
|
||||||
|
"list", help="list available schemes"
|
||||||
|
)
|
||||||
list_parser.set_defaults(cls=scheme.List)
|
list_parser.set_defaults(cls=scheme.List)
|
||||||
list_parser.add_argument("-n", "--names", action="store_true", help="list scheme names")
|
list_parser.add_argument(
|
||||||
list_parser.add_argument("-f", "--flavours", action="store_true", help="list scheme flavours")
|
"-n", "--names", action="store_true", help="list scheme names"
|
||||||
list_parser.add_argument("-m", "--modes", action="store_true", help="list scheme modes")
|
)
|
||||||
list_parser.add_argument("-v", "--variants", action="store_true", help="list scheme variants")
|
list_parser.add_argument(
|
||||||
|
"-f", "--flavours", action="store_true", help="list scheme flavours"
|
||||||
|
)
|
||||||
|
list_parser.add_argument(
|
||||||
|
"-m", "--modes", action="store_true", help="list scheme modes"
|
||||||
|
)
|
||||||
|
list_parser.add_argument(
|
||||||
|
"-v", "--variants", action="store_true", help="list scheme variants"
|
||||||
|
)
|
||||||
|
|
||||||
get_parser = scheme_command_parser.add_parser("get", help="get scheme properties")
|
get_parser = scheme_command_parser.add_parser("get", help="get scheme properties")
|
||||||
get_parser.set_defaults(cls=scheme.Get)
|
get_parser.set_defaults(cls=scheme.Get)
|
||||||
get_parser.add_argument("-n", "--name", action="store_true", help="print the current scheme name")
|
get_parser.add_argument(
|
||||||
get_parser.add_argument("-f", "--flavour", action="store_true", help="print the current scheme flavour")
|
"-n", "--name", action="store_true", help="print the current scheme name"
|
||||||
get_parser.add_argument("-m", "--mode", action="store_true", help="print the current scheme mode")
|
)
|
||||||
get_parser.add_argument("-v", "--variant", action="store_true", help="print the current scheme variant")
|
get_parser.add_argument(
|
||||||
|
"-f", "--flavour", action="store_true", help="print the current scheme flavour"
|
||||||
|
)
|
||||||
|
get_parser.add_argument(
|
||||||
|
"-m", "--mode", action="store_true", help="print the current scheme mode"
|
||||||
|
)
|
||||||
|
get_parser.add_argument(
|
||||||
|
"-v", "--variant", action="store_true", help="print the current scheme variant"
|
||||||
|
)
|
||||||
|
|
||||||
set_parser = scheme_command_parser.add_parser("set", help="set the current scheme")
|
set_parser = scheme_command_parser.add_parser("set", help="set the current scheme")
|
||||||
set_parser.set_defaults(cls=scheme.Set)
|
set_parser.set_defaults(cls=scheme.Set)
|
||||||
set_parser.add_argument("--notify", action="store_true", help="send a notification on error")
|
set_parser.add_argument(
|
||||||
set_parser.add_argument("-r", "--random", action="store_true", help="switch to a random scheme")
|
"--notify", action="store_true", help="send a notification on error"
|
||||||
set_parser.add_argument("-n", "--name", choices=get_scheme_names(), help="the name of the scheme to switch to")
|
)
|
||||||
|
set_parser.add_argument(
|
||||||
|
"-r", "--random", action="store_true", help="switch to a random scheme"
|
||||||
|
)
|
||||||
|
set_parser.add_argument(
|
||||||
|
"-n",
|
||||||
|
"--name",
|
||||||
|
choices=get_scheme_names(),
|
||||||
|
help="the name of the scheme to switch to",
|
||||||
|
)
|
||||||
set_parser.add_argument("-f", "--flavour", help="the flavour to switch to")
|
set_parser.add_argument("-f", "--flavour", help="the flavour to switch to")
|
||||||
set_parser.add_argument("-m", "--mode", choices=["dark", "light"], help="the mode to switch to")
|
set_parser.add_argument(
|
||||||
set_parser.add_argument("-v", "--variant", choices=scheme_variants, help="the variant to switch to")
|
"-m", "--mode", choices=["dark", "light"], help="the mode to switch to"
|
||||||
|
)
|
||||||
|
set_parser.add_argument(
|
||||||
|
"-v", "--variant", choices=scheme_variants, help="the variant to switch to"
|
||||||
|
)
|
||||||
|
|
||||||
# Create parser for screenshot opts
|
# Create parser for screenshot opts
|
||||||
screenshot_parser = command_parser.add_parser("screenshot", help="take a screenshot")
|
screenshot_parser = command_parser.add_parser(
|
||||||
|
"screenshot", help="take a screenshot"
|
||||||
|
)
|
||||||
screenshot_parser.set_defaults(cls=screenshot.Command)
|
screenshot_parser.set_defaults(cls=screenshot.Command)
|
||||||
screenshot_parser.add_argument("-r", "--region", nargs="?", const="slurp", help="take a screenshot of a region")
|
|
||||||
screenshot_parser.add_argument(
|
screenshot_parser.add_argument(
|
||||||
"-f", "--freeze", action="store_true", help="freeze the screen while selecting a region"
|
"-r", "--region", nargs="?", const="slurp", help="take a screenshot of a region"
|
||||||
|
)
|
||||||
|
screenshot_parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--freeze",
|
||||||
|
action="store_true",
|
||||||
|
help="freeze the screen while selecting a region",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create parser for record opts
|
# Create parser for record opts
|
||||||
record_parser = command_parser.add_parser("record", help="start a screen recording")
|
record_parser = command_parser.add_parser("record", help="start a screen recording")
|
||||||
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(
|
||||||
record_parser.add_argument("-s", "--sound", action="store_true", help="record audio")
|
"-r", "--region", nargs="?", const="slurp", help="record a region"
|
||||||
record_parser.add_argument("-p", "--pause", action="store_true", help="pause/resume the recording")
|
)
|
||||||
record_parser.add_argument("-c", "--clipboard", action="store_true", help="copy recording path to clipboard")
|
record_parser.add_argument(
|
||||||
|
"-s", "--sound", action="store_true", help="record audio"
|
||||||
|
)
|
||||||
|
|
||||||
# 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"
|
||||||
|
)
|
||||||
clipboard_parser.set_defaults(cls=clipboard.Command)
|
clipboard_parser.set_defaults(cls=clipboard.Command)
|
||||||
clipboard_parser.add_argument("-d", "--delete", action="store_true", help="delete from clipboard history")
|
clipboard_parser.add_argument(
|
||||||
|
"-d", "--delete", action="store_true", help="delete from clipboard history"
|
||||||
|
)
|
||||||
|
|
||||||
# Create parser for emoji-picker opts
|
# Create parser for emoji-picker opts
|
||||||
emoji_parser = command_parser.add_parser("emoji", help="emoji/glyph utilities")
|
emoji_parser = command_parser.add_parser("emoji", help="emoji/glyph utilities")
|
||||||
emoji_parser.set_defaults(cls=emoji.Command)
|
emoji_parser.set_defaults(cls=emoji.Command)
|
||||||
emoji_parser.add_argument("-p", "--picker", action="store_true", help="open the emoji/glyph picker")
|
emoji_parser.add_argument(
|
||||||
emoji_parser.add_argument("-f", "--fetch", action="store_true", help="fetch emoji/glyph data from remote")
|
"-p", "--picker", action="store_true", help="open the emoji/glyph picker"
|
||||||
|
)
|
||||||
|
emoji_parser.add_argument(
|
||||||
|
"-f", "--fetch", action="store_true", help="fetch emoji/glyph data from remote"
|
||||||
|
)
|
||||||
|
|
||||||
# Create parser for wallpaper opts
|
# Create parser for wallpaper opts
|
||||||
wallpaper_parser = command_parser.add_parser("wallpaper", help="manage the wallpaper")
|
wallpaper_parser = command_parser.add_parser(
|
||||||
|
"wallpaper", help="manage the wallpaper"
|
||||||
|
)
|
||||||
wallpaper_parser.set_defaults(cls=wallpaper.Command)
|
wallpaper_parser.set_defaults(cls=wallpaper.Command)
|
||||||
wallpaper_parser.add_argument(
|
wallpaper_parser.add_argument(
|
||||||
"-p", "--print", nargs="?", const=get_wallpaper(), metavar="PATH", help="print the scheme for a wallpaper"
|
"-p",
|
||||||
|
"--print",
|
||||||
|
nargs="?",
|
||||||
|
const=get_wallpaper(),
|
||||||
|
metavar="PATH",
|
||||||
|
help="print the scheme for a wallpaper",
|
||||||
)
|
)
|
||||||
wallpaper_parser.add_argument(
|
wallpaper_parser.add_argument(
|
||||||
"-r", "--random", nargs="?", const=wallpapers_dir, metavar="DIR", help="switch to a random wallpaper"
|
"-r",
|
||||||
|
"--random",
|
||||||
|
nargs="?",
|
||||||
|
const=wallpapers_dir,
|
||||||
|
metavar="DIR",
|
||||||
|
help="switch to a random wallpaper",
|
||||||
|
)
|
||||||
|
wallpaper_parser.add_argument(
|
||||||
|
"-f", "--file", help="the path to the wallpaper to switch to"
|
||||||
|
)
|
||||||
|
wallpaper_parser.add_argument(
|
||||||
|
"-n", "--no-filter", action="store_true", help="do not filter by size"
|
||||||
)
|
)
|
||||||
wallpaper_parser.add_argument("-f", "--file", help="the path to the wallpaper to switch to")
|
|
||||||
wallpaper_parser.add_argument("-n", "--no-filter", action="store_true", help="do not filter by size")
|
|
||||||
wallpaper_parser.add_argument(
|
wallpaper_parser.add_argument(
|
||||||
"-t",
|
"-t",
|
||||||
"--threshold",
|
"--threshold",
|
||||||
@@ -125,82 +207,11 @@ def parse_args() -> tuple[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 resizer opts
|
# Create parser for pip opts
|
||||||
resizer_parser = command_parser.add_parser("resizer", help="window resizer daemon")
|
pip_parser = command_parser.add_parser("pip", help="picture in picture utilities")
|
||||||
resizer_parser.set_defaults(cls=resizer.Command)
|
pip_parser.set_defaults(cls=pip.Command)
|
||||||
resizer_parser.add_argument("-d", "--daemon", action="store_true", help="start the resizer daemon")
|
pip_parser.add_argument(
|
||||||
resizer_parser.add_argument(
|
"-d", "--daemon", action="store_true", help="start the daemon"
|
||||||
"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)")
|
|
||||||
|
|
||||||
# Create parser for install opts
|
|
||||||
install_parser = command_parser.add_parser(
|
|
||||||
"install",
|
|
||||||
help="install the Caelestia dotfiles",
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
install_parser.set_defaults(cls=install.Command)
|
|
||||||
install_parser.add_argument("--aur-helper", choices=AUR_HELPERS, help="the AUR helper to use")
|
|
||||||
install_parser.add_argument(
|
|
||||||
"--enable-components", metavar="LIST", help="comma-separated list of components to enable"
|
|
||||||
)
|
|
||||||
install_parser.add_argument(
|
|
||||||
"--disable-components", metavar="LIST", help="comma-separated list of components to disable"
|
|
||||||
)
|
|
||||||
install_parser.add_argument("--noconfirm", action="store_true", help="use defaults for all prompts")
|
|
||||||
_set_install_epilog(install_parser)
|
|
||||||
|
|
||||||
# Create parser for update opts
|
|
||||||
update_parser = command_parser.add_parser("update", help="update the Caelestia dotfiles")
|
|
||||||
update_parser.set_defaults(cls=update.Command)
|
|
||||||
update_parser.add_argument("--aur-helper", choices=AUR_HELPERS, help="the AUR helper to use")
|
|
||||||
update_parser.add_argument("--noconfirm", action="store_true", help="use defaults for all prompts")
|
|
||||||
|
|
||||||
return parser, parser.parse_args()
|
return parser, parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def _set_install_epilog(install_parser: argparse.ArgumentParser) -> None:
|
|
||||||
"""Add components if using install subcommand"""
|
|
||||||
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == "install":
|
|
||||||
manifest = _load_install_manifest()
|
|
||||||
if manifest is not None and manifest.components:
|
|
||||||
install_parser.epilog = _components_epilog(manifest)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_install_manifest() -> Manifest | None:
|
|
||||||
source = DotsSource()
|
|
||||||
try:
|
|
||||||
source.ensure()
|
|
||||||
return source.manifest_at(source.remote_ref)
|
|
||||||
except Exception as e:
|
|
||||||
warn(f"failed to load manifest from dots repo ({e})\n", prefix=False)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _components_epilog(manifest: Manifest) -> str:
|
|
||||||
def e(*v: int) -> str:
|
|
||||||
return f"\033[{';'.join(str(c) for c in v)}m"
|
|
||||||
|
|
||||||
def b(c: int) -> str:
|
|
||||||
return e(1, c)
|
|
||||||
|
|
||||||
reset = e(0)
|
|
||||||
|
|
||||||
width = max(len(name) for name in manifest.components)
|
|
||||||
lines = [f"{b(34)}available components (for --enable-components / --disable-components):{reset}"]
|
|
||||||
for name, comp in manifest.components.items():
|
|
||||||
lines.append(f" {b(32)}{name:<{width}}{reset}\t{'(default)' if comp.default else '(off)'}")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|||||||
@@ -1,266 +0,0 @@
|
|||||||
import shutil
|
|
||||||
import textwrap
|
|
||||||
from argparse import Namespace
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils.dots.deployer import Deployer
|
|
||||||
from caelestia.utils.dots.legacy import (
|
|
||||||
LEGACY_META_PKG,
|
|
||||||
detect_legacy_repo,
|
|
||||||
legacy_config_symlinks,
|
|
||||||
legacy_symlinks,
|
|
||||||
legacy_to_delete,
|
|
||||||
)
|
|
||||||
from caelestia.utils.dots.manifest import ComponentError, Manifest, ManifestError
|
|
||||||
from caelestia.utils.dots.misc import build_local_packages, run_hooks
|
|
||||||
from caelestia.utils.dots.packages import DEFAULT_AUR_HELPER, PackageError, PackageInstaller
|
|
||||||
from caelestia.utils.dots.source import DotsSource, SourceError
|
|
||||||
from caelestia.utils.dots.state import DotsState
|
|
||||||
from caelestia.utils.io import confirm, disable_input, fatal, info, log, pause, prompt_selection, warn
|
|
||||||
from caelestia.utils.paths import (
|
|
||||||
config_backup_dir,
|
|
||||||
config_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_list_arg(value: str | None) -> list[str] | None:
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
return [item.strip() for item in value.split(",") if item.strip()]
|
|
||||||
|
|
||||||
|
|
||||||
def _deref_symlink(link: Path, target: Path) -> None:
|
|
||||||
"""Replace symlink `link` with a real copy of `target`'s content."""
|
|
||||||
|
|
||||||
bak = link.rename(link.parent / f"{link.name}.bak")
|
|
||||||
try:
|
|
||||||
if target.is_dir():
|
|
||||||
shutil.copytree(target, link, symlinks=True)
|
|
||||||
else:
|
|
||||||
shutil.copy2(target, link)
|
|
||||||
except OSError:
|
|
||||||
bak.rename(link)
|
|
||||||
raise
|
|
||||||
bak.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
|
||||||
args: Namespace
|
|
||||||
|
|
||||||
def __init__(self, args: Namespace) -> None:
|
|
||||||
self.args = args
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
if self.args.noconfirm:
|
|
||||||
disable_input()
|
|
||||||
|
|
||||||
self.print_greeting()
|
|
||||||
self.create_backup()
|
|
||||||
legacy_dir = detect_legacy_repo() # Detect legacy repo first cause deploy overwrites legacy syms
|
|
||||||
|
|
||||||
source, tip, manifest = self.fetch_manifest()
|
|
||||||
try:
|
|
||||||
installer, packages, local_packages = self.install_packages(source, manifest)
|
|
||||||
except PackageError as e:
|
|
||||||
fatal(e)
|
|
||||||
run_hooks(manifest, "post_package")
|
|
||||||
self.dereference_legacy(legacy_dir) # Copy legacy content into place before deploy overwrites the symlinks
|
|
||||||
deployed = self.deploy_configs(source, manifest)
|
|
||||||
run_hooks(manifest, "post_install")
|
|
||||||
|
|
||||||
DotsState(
|
|
||||||
aur_helper=getattr(installer, "helper", DEFAULT_AUR_HELPER),
|
|
||||||
applied_rev=tip,
|
|
||||||
enabled_components=manifest.enabled_components,
|
|
||||||
packages=packages,
|
|
||||||
local_packages=local_packages,
|
|
||||||
deployed_files=deployed,
|
|
||||||
).save()
|
|
||||||
|
|
||||||
self.migrate_legacy(installer, legacy_dir)
|
|
||||||
self.print_done()
|
|
||||||
|
|
||||||
def print_greeting(self) -> None:
|
|
||||||
print(
|
|
||||||
"\033[38;2;150;241;241m" # Caelestia colour
|
|
||||||
+ textwrap.dedent(
|
|
||||||
r"""
|
|
||||||
╭─────────────────────────────────────────────────╮
|
|
||||||
│ ______ __ __ _ │
|
|
||||||
│ / ____/___ ____ / /__ _____/ /_(_)___ _ │
|
|
||||||
│ / / / __ `/ _ \/ / _ \/ ___/ __/ / __ `/ │
|
|
||||||
│ / /___/ /_/ / __/ / __(__ ) /_/ / /_/ / │
|
|
||||||
│ \____/\__,_/\___/_/\___/____/\__/_/\__,_/ │
|
|
||||||
│ │
|
|
||||||
╰─────────────────────────────────────────────────╯
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
+ "\033[0m"
|
|
||||||
)
|
|
||||||
info("Welcome to the Caelestia dotfiles installer!")
|
|
||||||
info("Here's a quick overview on what this command is going to do:")
|
|
||||||
info(" - Install dependencies")
|
|
||||||
info(" - Install config files")
|
|
||||||
info("The installer does NOT set up hardware/system level configs (e.g. drivers). Please do this yourself.")
|
|
||||||
pause()
|
|
||||||
print()
|
|
||||||
|
|
||||||
def create_backup(self) -> None:
|
|
||||||
if config_dir.exists():
|
|
||||||
if not confirm("Back up the config directory?", default=True):
|
|
||||||
return
|
|
||||||
|
|
||||||
log(f"Creating a backup of {config_dir}...")
|
|
||||||
if config_backup_dir.exists():
|
|
||||||
if not confirm("A backup already exists, overwrite?", default=False):
|
|
||||||
info("Not creating backup.")
|
|
||||||
return
|
|
||||||
|
|
||||||
log("Deleting old backup...")
|
|
||||||
shutil.rmtree(config_backup_dir)
|
|
||||||
|
|
||||||
shutil.copytree(config_dir, config_backup_dir, symlinks=True)
|
|
||||||
info(f"Created backup at {config_backup_dir}")
|
|
||||||
|
|
||||||
def fetch_manifest(self) -> tuple[DotsSource, str, Manifest]:
|
|
||||||
print()
|
|
||||||
log("Fetching dots repo...")
|
|
||||||
source = DotsSource()
|
|
||||||
try:
|
|
||||||
source.ensure()
|
|
||||||
tip = source.checkout_tip()
|
|
||||||
except SourceError as e:
|
|
||||||
fatal(e)
|
|
||||||
|
|
||||||
enable = _parse_list_arg(self.args.enable_components)
|
|
||||||
disable = _parse_list_arg(self.args.disable_components)
|
|
||||||
try:
|
|
||||||
manifest = source.manifest_at(tip)
|
|
||||||
|
|
||||||
# No flags given, prompt user for non-default components
|
|
||||||
if enable is None and disable is None:
|
|
||||||
optional = [name for name, comp in manifest.components.items() if not comp.default]
|
|
||||||
if optional:
|
|
||||||
enable = prompt_selection(optional, "Components to enable?")
|
|
||||||
|
|
||||||
manifest.resolve_components(enable=enable, disable=disable)
|
|
||||||
except (SourceError, ManifestError, ComponentError) as e:
|
|
||||||
fatal(e)
|
|
||||||
|
|
||||||
names = ", ".join(manifest.enabled_components) or "none"
|
|
||||||
info(f"Enabled components: {names}")
|
|
||||||
|
|
||||||
return source, tip, manifest
|
|
||||||
|
|
||||||
def deploy_configs(self, source: DotsSource, manifest: Manifest) -> dict[str, str]:
|
|
||||||
print()
|
|
||||||
log("Installing configs...")
|
|
||||||
deployer = Deployer()
|
|
||||||
for entry in manifest.enabled_entries():
|
|
||||||
src = source.working_path(entry.expanded_src())
|
|
||||||
if not src.exists():
|
|
||||||
warn(f"missing in source, skipping: {entry.src}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
dests = entry.expanded_dests()
|
|
||||||
if not dests:
|
|
||||||
warn(f"dest glob matched nothing, skipping: {entry.dest}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
for dest in dests:
|
|
||||||
deployer.place(src, Path(dest))
|
|
||||||
info(f"{entry.src} -> {dest}")
|
|
||||||
|
|
||||||
return deployer.deployed_files
|
|
||||||
|
|
||||||
def install_packages(
|
|
||||||
self, source: DotsSource, manifest: Manifest
|
|
||||||
) -> tuple[PackageInstaller, list[str], dict[str, list[str]]]:
|
|
||||||
installer = PackageInstaller.get(self.args.aur_helper, self.args.noconfirm)
|
|
||||||
|
|
||||||
packages = manifest.enabled_packages()
|
|
||||||
if packages:
|
|
||||||
print()
|
|
||||||
log("Installing packages...")
|
|
||||||
installer.install(packages)
|
|
||||||
|
|
||||||
local_packages = {}
|
|
||||||
local_dirs = manifest.enabled_local_packages()
|
|
||||||
if local_dirs:
|
|
||||||
print()
|
|
||||||
log("Building local packages...")
|
|
||||||
local_packages = build_local_packages(installer, source, local_dirs)
|
|
||||||
|
|
||||||
return installer, packages, local_packages
|
|
||||||
|
|
||||||
def dereference_legacy(self, legacy_dir: Path | None) -> None:
|
|
||||||
"""Replace legacy symlinks with real copies of their targets."""
|
|
||||||
|
|
||||||
symlinks = legacy_symlinks(legacy_dir)
|
|
||||||
if not symlinks:
|
|
||||||
return
|
|
||||||
|
|
||||||
print()
|
|
||||||
log("Preserving content from legacy symlinks...")
|
|
||||||
for path in symlinks:
|
|
||||||
target = path.resolve()
|
|
||||||
if not target.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
_deref_symlink(path, target)
|
|
||||||
info(f"Copied {target} -> {path}")
|
|
||||||
except OSError as e:
|
|
||||||
warn(f"failed to preserve {path}: {e}")
|
|
||||||
|
|
||||||
def deref_backup_syms(self, legacy_dir: Path | None) -> None:
|
|
||||||
"""Deref the backup's legacy symlinks before the repo is cleared, so the backup keeps real content."""
|
|
||||||
|
|
||||||
if not config_backup_dir.is_dir():
|
|
||||||
return
|
|
||||||
|
|
||||||
for link in legacy_config_symlinks(config_backup_dir, legacy_dir):
|
|
||||||
target = link.resolve()
|
|
||||||
if not target.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
_deref_symlink(link, target)
|
|
||||||
except OSError as e:
|
|
||||||
warn(f"failed to preserve {link} in backup: {e}")
|
|
||||||
|
|
||||||
def migrate_legacy(self, installer: PackageInstaller, legacy_dir: Path | None) -> None:
|
|
||||||
"""Clean up a previous install.fish setup (repo, symlinks and metapackage)."""
|
|
||||||
|
|
||||||
to_delete = legacy_to_delete(legacy_dir)
|
|
||||||
meta_installed = installer.is_installed(LEGACY_META_PKG)
|
|
||||||
if not to_delete and not meta_installed:
|
|
||||||
return
|
|
||||||
|
|
||||||
print()
|
|
||||||
log("Found a legacy Caelestia installation...")
|
|
||||||
if not confirm("Clear legacy installation?"):
|
|
||||||
return
|
|
||||||
|
|
||||||
deployer = Deployer()
|
|
||||||
try:
|
|
||||||
self.deref_backup_syms(legacy_dir)
|
|
||||||
for path in to_delete:
|
|
||||||
deployer.remove(path)
|
|
||||||
info(f"Deleted {path}")
|
|
||||||
|
|
||||||
if meta_installed:
|
|
||||||
log("Removing legacy meta package...")
|
|
||||||
installer.remove([LEGACY_META_PKG])
|
|
||||||
except (OSError, PackageError) as e:
|
|
||||||
warn(f"could not fully clear the legacy installation: {e}")
|
|
||||||
|
|
||||||
def print_done(self) -> None:
|
|
||||||
print()
|
|
||||||
info("All done! Caelestia has been installed.")
|
|
||||||
info("A few things to finish up:")
|
|
||||||
info(" - A reboot is recommended for all changes take effect")
|
|
||||||
info(" - Edit `~/.config/caelestia/hypr-vars.conf` to set default apps, keybinds and much more")
|
|
||||||
info(" - Edit `~/.config/caelestia/hypr-user.conf` to set your monitor layout and other Hyprland configs")
|
|
||||||
info(" - Run `caelestia update` later to pull in the latest changes")
|
|
||||||
info("Enjoy! For support (or to just hang out), join our Discord server: https://discord.gg/BGDCFCmMBk")
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import re
|
||||||
|
import socket
|
||||||
|
from argparse import Namespace
|
||||||
|
|
||||||
|
from caelestia.utils import hypr
|
||||||
|
|
||||||
|
|
||||||
|
class Command:
|
||||||
|
args: Namespace
|
||||||
|
|
||||||
|
def __init__(self, args: Namespace) -> None:
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
if self.args.daemon:
|
||||||
|
self.daemon()
|
||||||
|
else:
|
||||||
|
win = hypr.message("activewindow")
|
||||||
|
if win["floating"]:
|
||||||
|
self.handle_window(win["address"], win["workspace"]["name"])
|
||||||
|
|
||||||
|
def handle_window(self, address: str, ws: str) -> None:
|
||||||
|
mon_id = next(w for w in hypr.message("workspaces") if w["name"] == ws)["monitorID"]
|
||||||
|
mon = next(m for m in hypr.message("monitors") if m["id"] == mon_id)
|
||||||
|
width, height = next(c for c in hypr.message("clients") if c["address"] == address)["size"]
|
||||||
|
|
||||||
|
scale_factor = mon["height"] / 4 / height
|
||||||
|
scaled_win_size = f"{int(width * scale_factor)} {int(height * scale_factor)}"
|
||||||
|
off = min(mon["width"], mon["height"]) * 0.03
|
||||||
|
move_to = f"{int(mon['width'] - off - width * scale_factor)} {int(mon['height'] - off - height * scale_factor)}"
|
||||||
|
|
||||||
|
hypr.dispatch("resizewindowpixel", "exact", f"{scaled_win_size},address:{address}")
|
||||||
|
hypr.dispatch("movewindowpixel", "exact", f"{move_to},address:{address}")
|
||||||
|
|
||||||
|
def daemon(self) -> None:
|
||||||
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.connect(hypr.socket2_path)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data = sock.recv(4096).decode()
|
||||||
|
if data.startswith("openwindow>>"):
|
||||||
|
address, ws, cls, title = data[12:].split(",")
|
||||||
|
if re.match(r"^[Pp]icture(-| )in(-| )[Pp]icture$", title):
|
||||||
|
self.handle_window(f"0x{address}", ws)
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
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 pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils import hypr
|
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 get_config, recording_notif_path, recording_path, recordings_dir
|
|
||||||
|
|
||||||
RECORDER = "gpu-screen-recorder"
|
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
@@ -20,97 +14,72 @@ class Command:
|
|||||||
self.args = args
|
self.args = args
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
if self.args.pause:
|
proc = subprocess.run(["pidof", "wl-screenrec"])
|
||||||
subprocess.run(["pkill", "-USR2", "-f", RECORDER], stdout=subprocess.DEVNULL)
|
if proc.returncode == 0:
|
||||||
elif self.proc_running():
|
|
||||||
self.stop()
|
self.stop()
|
||||||
else:
|
else:
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def proc_running(self) -> bool:
|
|
||||||
return subprocess.run(["pidof", RECORDER], stdout=subprocess.DEVNULL).returncode == 0
|
|
||||||
|
|
||||||
def intersects(self, a: tuple[int, int, int, int], b: tuple[int, int, int, int]) -> bool:
|
|
||||||
return a[0] < b[0] + b[2] and a[0] + a[2] > b[0] and a[1] < b[1] + b[3] and a[1] + a[3] > b[1]
|
|
||||||
|
|
||||||
def start(self) -> None:
|
def start(self) -> None:
|
||||||
args = ["-w"]
|
args = []
|
||||||
|
|
||||||
monitors = hypr.message("monitors")
|
|
||||||
if self.args.region:
|
if self.args.region:
|
||||||
if self.args.region == "slurp":
|
if self.args.region == "slurp":
|
||||||
region = subprocess.check_output(["slurp", "-f", "%wx%h+%x+%y"], text=True)
|
region = subprocess.check_output(["slurp"], text=True)
|
||||||
else:
|
else:
|
||||||
region = self.args.region.strip()
|
region = self.args.region
|
||||||
args += ["region", "-region", region]
|
args += ["-g", region.strip()]
|
||||||
|
|
||||||
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:
|
||||||
args += ["-a", "default_output"]
|
sources = subprocess.check_output(["pactl", "list", "short", "sources"], text=True).splitlines()
|
||||||
|
for source in sources:
|
||||||
config = get_config()
|
if "RUNNING" in source:
|
||||||
try:
|
args += ["--audio", "--audio-device", source.split()[1]]
|
||||||
if "record" in config and "extraArgs" in config["record"]:
|
break
|
||||||
args += config["record"]["extraArgs"]
|
else:
|
||||||
except TypeError as e:
|
raise ValueError("No audio source found")
|
||||||
raise ValueError(f"Config option 'record.extraArgs' should be an array: {e}")
|
|
||||||
|
|
||||||
recording_path.parent.mkdir(parents=True, exist_ok=True)
|
recording_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
proc = subprocess.Popen([RECORDER, *args, "-o", str(recording_path)], start_new_session=True)
|
proc = subprocess.Popen(
|
||||||
|
["wl-screenrec", *args, "--codec", "hevc", "-f", recording_path],
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
|
||||||
notif = notify("-p", "Recording started", "Recording...")
|
# Send notif if proc hasn't ended after a small delay
|
||||||
recording_notif_path.write_text(notif)
|
time.sleep(0.1)
|
||||||
|
if proc.poll() is None:
|
||||||
try:
|
notif = notify("-p", "Recording started", "Recording...")
|
||||||
if proc.wait(1) != 0:
|
recording_notif_path.write_text(notif)
|
||||||
close_notification(notif)
|
else:
|
||||||
notify(
|
notify("Recording failed", f"Recording failed to start: {proc.communicate()[1]}")
|
||||||
"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:
|
||||||
# Start killing recording process
|
subprocess.run(["pkill", "wl-screenrec"])
|
||||||
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)
|
||||||
shutil.move(recording_path, new_path)
|
recording_path.rename(new_path)
|
||||||
|
|
||||||
# Close start notification
|
# Close start notification
|
||||||
try:
|
try:
|
||||||
close_notification(recording_notif_path.read_text())
|
notif = recording_notif_path.read_text()
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"gdbus",
|
||||||
|
"call",
|
||||||
|
"--session",
|
||||||
|
"--dest=org.freedesktop.Notifications",
|
||||||
|
"--object-path=/org/freedesktop/Notifications",
|
||||||
|
"--method=org.freedesktop.Notifications.CloseNotification",
|
||||||
|
notif,
|
||||||
|
]
|
||||||
|
)
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.args.clipboard:
|
|
||||||
file_uri = Path(new_path).resolve().as_uri() + "\n"
|
|
||||||
subprocess.run(["wl-copy", "--type", "text/uri-list"], input=file_uri.encode())
|
|
||||||
|
|
||||||
action = notify(
|
action = notify(
|
||||||
"--action=watch=Watch",
|
"--action=watch=Watch",
|
||||||
"--action=open=Open",
|
"--action=open=Open",
|
||||||
@@ -120,7 +89,7 @@ class Command:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if action == "watch":
|
if action == "watch":
|
||||||
subprocess.Popen(["xdg-open", new_path], start_new_session=True)
|
subprocess.Popen(["app2unit", "-O", new_path], start_new_session=True)
|
||||||
elif action == "open":
|
elif action == "open":
|
||||||
p = subprocess.run(
|
p = subprocess.run(
|
||||||
[
|
[
|
||||||
@@ -135,6 +104,6 @@ class Command:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
subprocess.Popen(["xdg-open", new_path.parent], start_new_session=True)
|
subprocess.Popen(["app2unit", "-O", new_path.parent], start_new_session=True)
|
||||||
elif action == "delete":
|
elif action == "delete":
|
||||||
new_path.unlink()
|
new_path.unlink()
|
||||||
|
|||||||
@@ -1,481 +0,0 @@
|
|||||||
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.io import error, fatal, info, log, warn
|
|
||||||
from caelestia.utils.paths import get_config
|
|
||||||
|
|
||||||
|
|
||||||
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 _make_resize_cmd(self, width: int | str, height: int | str, address: str) -> str:
|
|
||||||
if hypr.is_lua_config():
|
|
||||||
return f'dispatch hl.dsp.window.resize({{x = {width}, y = {height}, exact = true, window = "address:{address}"}})'
|
|
||||||
return f"dispatch resizewindowpixel exact {width} {height},address:{address}"
|
|
||||||
|
|
||||||
def _make_move_cmd(self, x: int, y: int, address: str) -> str:
|
|
||||||
if hypr.is_lua_config():
|
|
||||||
return f'dispatch hl.dsp.window.move({{x = {x}, y = {y}, window = "address:{address}"}})'
|
|
||||||
return f"dispatch movewindowpixel exact {x} {y},address:{address}"
|
|
||||||
|
|
||||||
def _make_float_cmd(self, address: str) -> str:
|
|
||||||
if hypr.is_lua_config():
|
|
||||||
return f'dispatch hl.dsp.window.float({{action = "toggle", window = "address:{address}"}})'
|
|
||||||
return f"dispatch togglefloating address:{address}"
|
|
||||||
|
|
||||||
def _make_center_cmd(self) -> str:
|
|
||||||
if hypr.is_lua_config():
|
|
||||||
return "dispatch hl.dsp.window.center()"
|
|
||||||
return "dispatch centerwindow"
|
|
||||||
|
|
||||||
def _load_window_rules(self) -> list[WindowRule]:
|
|
||||||
default_rules = [
|
|
||||||
WindowRule("(Bitwarden", "titleContains", "20%", "54%", ["float", "center"]),
|
|
||||||
WindowRule("^[Pp]icture(-| )in(-| )[Pp]icture$", "titleRegex", "", "", ["pip"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
config = get_config()
|
|
||||||
try:
|
|
||||||
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 KeyError:
|
|
||||||
warn("invalid config, falling back to default rules")
|
|
||||||
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 = self._make_resize_cmd(scaled_width, scaled_height, address)
|
|
||||||
command2 = self._make_move_cmd(int(move_x), int(move_y), address)
|
|
||||||
hypr.batch(command1, command2)
|
|
||||||
|
|
||||||
info(f"Applied PiP action to window {address}: {scaled_width}x{scaled_height} at ({move_x}, {move_y})")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error(f"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(self._make_float_cmd(f"0x{window_id}"))
|
|
||||||
|
|
||||||
if "pip" in actions:
|
|
||||||
self._apply_pip_action(window_id)
|
|
||||||
return True
|
|
||||||
|
|
||||||
dispatch_commands.append(self._make_resize_cmd(width, height, f"0x{window_id}"))
|
|
||||||
|
|
||||||
if "center" in actions:
|
|
||||||
dispatch_commands.append(self._make_center_cmd())
|
|
||||||
|
|
||||||
try:
|
|
||||||
hypr.batch(*dispatch_commands)
|
|
||||||
info(f"Applied actions to window 0x{window_id}: {width} x {height} ({', '.join(actions)})")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
error(f"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:
|
|
||||||
warn(f"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):
|
|
||||||
warn(f"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(f"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(f"Rate limited: skipping window 0x{window_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
info(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:
|
|
||||||
warn(f"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):
|
|
||||||
warn(f"invalid window ID format: {window_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
log(f"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(f"Rate limited: skipping window 0x{window_id}")
|
|
||||||
return
|
|
||||||
|
|
||||||
info(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:
|
|
||||||
warn(f"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:
|
|
||||||
info(
|
|
||||||
"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"):
|
|
||||||
error("no active window found")
|
|
||||||
return
|
|
||||||
|
|
||||||
address = active_window_result.get("address", "")
|
|
||||||
if not isinstance(address, str) or not address.startswith("0x"):
|
|
||||||
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):
|
|
||||||
warn(f"window '{window_title}' is not floating; PiP only works on floating windows.")
|
|
||||||
return
|
|
||||||
|
|
||||||
info(f"Applying PiP to active window: '{window_title}'")
|
|
||||||
self._apply_pip_action(window_id)
|
|
||||||
info("PiP applied successfully")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error(f"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:
|
|
||||||
warn(f"no windows found matching pattern '{temp_rule.name}' with match type '{temp_rule.match_type}'")
|
|
||||||
return
|
|
||||||
|
|
||||||
info(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", "")
|
|
||||||
|
|
||||||
info(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
|
|
||||||
|
|
||||||
info(f"Successfully applied rule to {success_count}/{len(matching_windows)} windows")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error(f"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"):
|
|
||||||
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"):
|
|
||||||
error("invalid window address")
|
|
||||||
return
|
|
||||||
|
|
||||||
window_id = address[2:] # Remove "0x" prefix
|
|
||||||
|
|
||||||
info(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:
|
|
||||||
info("Rule applied successfully")
|
|
||||||
else:
|
|
||||||
error("failed to apply rule")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error(f"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:
|
|
||||||
warn(f"invalid regex pattern '{temp_rule.name}'")
|
|
||||||
return []
|
|
||||||
|
|
||||||
if matches:
|
|
||||||
matching_windows.append(window)
|
|
||||||
|
|
||||||
return matching_windows
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error(f"failed to find matching windows: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _run_daemon(self) -> None:
|
|
||||||
info("Hyprland window resizer started")
|
|
||||||
info(f"Loaded {len(self.window_rules)} window rules")
|
|
||||||
|
|
||||||
socket_path = Path(hypr.socket2_path)
|
|
||||||
if not socket_path.exists():
|
|
||||||
fatal(f"Hyprland socket not found at {socket_path}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
||||||
sock.connect(hypr.socket2_path)
|
|
||||||
|
|
||||||
info("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:
|
|
||||||
info("Resizer daemon stopped")
|
|
||||||
except Exception as e:
|
|
||||||
error(str(e))
|
|
||||||
@@ -2,7 +2,6 @@ import subprocess
|
|||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from caelestia.utils import hypr
|
|
||||||
from caelestia.utils.notify import notify
|
from caelestia.utils.notify import notify
|
||||||
from caelestia.utils.paths import screenshots_cache_dir, screenshots_dir
|
from caelestia.utils.paths import screenshots_cache_dir, screenshots_dir
|
||||||
|
|
||||||
@@ -27,19 +26,11 @@ class Command:
|
|||||||
else:
|
else:
|
||||||
sc_data = subprocess.check_output(["grim", "-l", "0", "-g", self.args.region.strip(), "-"])
|
sc_data = subprocess.check_output(["grim", "-l", "0", "-g", self.args.region.strip(), "-"])
|
||||||
swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True)
|
swappy = subprocess.Popen(["swappy", "-f", "-"], stdin=subprocess.PIPE, start_new_session=True)
|
||||||
|
swappy.stdin.write(sc_data)
|
||||||
# Ensure stdin is not None for the type checker
|
swappy.stdin.close()
|
||||||
if swappy.stdin:
|
|
||||||
swappy.stdin.write(sc_data)
|
|
||||||
swappy.stdin.close()
|
|
||||||
|
|
||||||
def fullscreen(self) -> None:
|
def fullscreen(self) -> None:
|
||||||
cmd = ["grim"]
|
sc_data = subprocess.check_output(["grim", "-"])
|
||||||
focused_monitor = next(monitor for monitor in hypr.message("monitors") if monitor["focused"])
|
|
||||||
if focused_monitor:
|
|
||||||
cmd += ["-o", focused_monitor["name"]]
|
|
||||||
cmd += ["-"]
|
|
||||||
sc_data = subprocess.check_output(cmd)
|
|
||||||
|
|
||||||
subprocess.run(["wl-copy"], input=sc_data)
|
subprocess.run(["wl-copy"], input=sc_data)
|
||||||
|
|
||||||
|
|||||||
@@ -17,30 +17,22 @@ 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"]
|
args = ["qs", "-c", "caelestia", "-n", "--log-rules", self.args.log_rules]
|
||||||
if self.args.log_rules:
|
|
||||||
args.extend(["--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)
|
||||||
else:
|
else:
|
||||||
shell = subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True)
|
shell = subprocess.Popen(args, stdout=subprocess.PIPE, universal_newlines=True)
|
||||||
|
for line in shell.stdout:
|
||||||
|
if self.filter_log(line):
|
||||||
|
print(line, end="")
|
||||||
|
|
||||||
# Ensure stdout is not None for the type checker
|
def shell(self, *args: list[str]) -> str:
|
||||||
if shell.stdout:
|
|
||||||
for line in shell.stdout:
|
|
||||||
if self.filter_log(line):
|
|
||||||
print(line, end="")
|
|
||||||
|
|
||||||
def shell(self, *args: str) -> str:
|
|
||||||
return subprocess.check_output(["qs", "-c", "caelestia", *args], text=True)
|
return subprocess.check_output(["qs", "-c", "caelestia", *args], text=True)
|
||||||
|
|
||||||
def filter_log(self, line: str) -> bool:
|
def filter_log(self, line: str) -> bool:
|
||||||
@@ -50,10 +42,7 @@ class Command:
|
|||||||
print(self.shell("ipc", "show"), end="")
|
print(self.shell("ipc", "show"), end="")
|
||||||
|
|
||||||
def print_log(self) -> None:
|
def print_log(self) -> None:
|
||||||
if self.args.log_rules:
|
log = self.shell("log", "-r", 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):
|
||||||
|
|||||||
@@ -1,164 +1,95 @@
|
|||||||
import json
|
import subprocess
|
||||||
import shlex
|
|
||||||
import shutil
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from collections import ChainMap
|
|
||||||
from typing import Any, Callable, cast
|
|
||||||
|
|
||||||
from caelestia.utils import hypr
|
from caelestia.utils import hypr
|
||||||
from caelestia.utils.paths import get_config
|
|
||||||
|
|
||||||
|
|
||||||
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 = 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(get_config()["toggles"], self.cfg)
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
if self.args.workspace == "specialws":
|
getattr(self, self.args.workspace)()
|
||||||
self.specialws()
|
|
||||||
return
|
|
||||||
|
|
||||||
spawned = False
|
def get_clients(self) -> list[dict[str, any]]:
|
||||||
if self.args.workspace in self.cfg:
|
|
||||||
for client in self.cfg[self.args.workspace].values():
|
|
||||||
if "enable" in client and client["enable"] and self.handle_client_config(client):
|
|
||||||
spawned = True
|
|
||||||
|
|
||||||
if not spawned:
|
|
||||||
hypr.dispatch("togglespecialworkspace", self.args.workspace)
|
|
||||||
|
|
||||||
def get_clients(self) -> list[dict[str, Any]]:
|
|
||||||
if self.clients is None:
|
if self.clients is None:
|
||||||
self.clients = cast(list[dict[str, Any]], hypr.message("clients"))
|
self.clients = hypr.message("clients")
|
||||||
|
|
||||||
return self.clients
|
return self.clients
|
||||||
|
|
||||||
def move_client(self, selector: Callable, workspace: str) -> None:
|
def move_client(self, selector: callable, workspace: str) -> None:
|
||||||
for client in self.get_clients():
|
for client in self.get_clients():
|
||||||
if selector(client) and client["workspace"]["name"] != f"special:{workspace}":
|
if selector(client):
|
||||||
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:
|
||||||
if (spawn[0].endswith(".desktop") or shutil.which(spawn[0])) and not any(
|
exists = any(selector(client) for client in self.get_clients())
|
||||||
selector(client) for client in self.get_clients()
|
|
||||||
):
|
|
||||||
hypr.dispatch("exec", f"[workspace special:{self.args.workspace}] {shlex.join(spawn)}")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def handle_client_config(self, client: dict[str, Any]) -> bool:
|
if not exists:
|
||||||
def selector(c: dict[str, Any]) -> bool:
|
subprocess.Popen(["app2unit", "--", *spawn], start_new_session=True)
|
||||||
# Each match is or, inside matches is and
|
|
||||||
for match in client["match"]:
|
|
||||||
if is_subset(c, match):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
spawned = False
|
return not exists
|
||||||
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)
|
|
||||||
|
|
||||||
return spawned
|
def spawn_or_move(
|
||||||
|
self, selector: callable, spawn: list[str], workspace: str
|
||||||
|
) -> None:
|
||||||
|
if not self.spawn_client(selector, spawn):
|
||||||
|
self.move_client(selector, workspace)
|
||||||
|
|
||||||
|
def communication(self) -> None:
|
||||||
|
self.spawn_or_move(
|
||||||
|
lambda c: c["class"] == "discord", ["discord"], "communication"
|
||||||
|
)
|
||||||
|
self.move_client(lambda c: c["class"] == "whatsapp", "communication")
|
||||||
|
hypr.dispatch("togglespecialworkspace", "communication")
|
||||||
|
|
||||||
|
def music(self) -> None:
|
||||||
|
self.spawn_or_move(
|
||||||
|
lambda c: c["class"] == "Spotify"
|
||||||
|
or c["initialTitle"] == "Spotify"
|
||||||
|
or c["initialTitle"] == "Spotify Free",
|
||||||
|
["spicetify", "watch", "-s"],
|
||||||
|
"music",
|
||||||
|
)
|
||||||
|
self.move_client(lambda c: c["class"] == "feishin", "music")
|
||||||
|
hypr.dispatch("togglespecialworkspace", "music")
|
||||||
|
|
||||||
|
def sysmon(self) -> None:
|
||||||
|
hypr.dispatch("togglespecialworkspace", "sysmon")
|
||||||
|
self.spawn_client(
|
||||||
|
lambda c: c["class"] == "kitty"
|
||||||
|
and c["title"] == "btop"
|
||||||
|
and c["workspace"]["name"] == "special:sysmon",
|
||||||
|
# ["foot", "-a", "btop", "-T", "btop", "fish", "-C", "exec btop"],
|
||||||
|
["kitty", "--detach", "-T", "btop", "fish", "-C", "btop"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def steam(self) -> None:
|
||||||
|
self.spawn_or_move(
|
||||||
|
lambda c: c["class"] == "steam" or c["initialTitle"] == "Steam",
|
||||||
|
["steam"],
|
||||||
|
"steam",
|
||||||
|
)
|
||||||
|
hypr.dispatch("togglespecialworkspace", "steam")
|
||||||
|
|
||||||
|
def todo(self) -> None:
|
||||||
|
hypr.dispatch("togglespecialworkspace", "todo")
|
||||||
|
self.spawn_or_move(lambda c: c["class"] == "Todoist", ["todoist"], "todo")
|
||||||
|
|
||||||
def specialws(self) -> None:
|
def specialws(self) -> None:
|
||||||
monitors = cast(list[dict[str, Any]], hypr.message("monitors"))
|
workspaces = hypr.message("workspaces")
|
||||||
target = next((m for m in monitors if m.get("focused")), None)
|
on_special_ws = any(ws["name"] == "special:special" for ws in workspaces)
|
||||||
if target:
|
toggle_ws = "special"
|
||||||
special = target.get("specialWorkspace", {}).get("name", "")[8:] or "special"
|
|
||||||
hypr.dispatch("togglespecialworkspace", 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)
|
||||||
|
|||||||
@@ -1,260 +0,0 @@
|
|||||||
import sys
|
|
||||||
from argparse import Namespace
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils.dots.deployer import Deployer
|
|
||||||
from caelestia.utils.dots.diff import Changeset
|
|
||||||
from caelestia.utils.dots.manifest import ComponentError, Manifest, ManifestError
|
|
||||||
from caelestia.utils.dots.misc import build_local_packages, run_hooks
|
|
||||||
from caelestia.utils.dots.packages import PackageError, PackageInstaller
|
|
||||||
from caelestia.utils.dots.source import DotsSource, SourceError
|
|
||||||
from caelestia.utils.dots.state import DotsState
|
|
||||||
from caelestia.utils.io import disable_input, fatal, info, log, prompt_selection, warn
|
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
|
||||||
args: Namespace
|
|
||||||
|
|
||||||
def __init__(self, args: Namespace) -> None:
|
|
||||||
self.args = args
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
if self.args.noconfirm:
|
|
||||||
disable_input()
|
|
||||||
|
|
||||||
state = DotsState.load()
|
|
||||||
if state.applied_rev is None:
|
|
||||||
fatal("dots not installed yet. Run `caelestia install` first.")
|
|
||||||
|
|
||||||
# Run system update
|
|
||||||
try:
|
|
||||||
installer = PackageInstaller.get(self.args.aur_helper or state.aur_helper, self.args.noconfirm)
|
|
||||||
installer.system_update()
|
|
||||||
except PackageError as e:
|
|
||||||
fatal(e)
|
|
||||||
|
|
||||||
# Get manifest or exit if up to date
|
|
||||||
source, tip, manifest = self.fetch_manifest(state, state.applied_rev)
|
|
||||||
|
|
||||||
# Apply file changes
|
|
||||||
entries = manifest.enabled_entries()
|
|
||||||
try:
|
|
||||||
changeset = Changeset.compute(source, state.applied_rev, tip, entries, state.deployed_files)
|
|
||||||
source.checkout_tip()
|
|
||||||
except SourceError as e:
|
|
||||||
fatal(e)
|
|
||||||
new_files, revived_files, placed = self.deploy_changeset(source, changeset)
|
|
||||||
|
|
||||||
# Persist file changes immediately so a later failure can't lose track of them
|
|
||||||
deployed = dict(state.deployed_files)
|
|
||||||
for dest in (*changeset.deletes, *changeset.stale, *changeset.untracked):
|
|
||||||
deployed.pop(str(dest), None)
|
|
||||||
for repofile, dest in changeset.remap:
|
|
||||||
deployed[str(dest)] = repofile
|
|
||||||
deployed.update(placed)
|
|
||||||
state.deployed_files = deployed
|
|
||||||
state.save()
|
|
||||||
|
|
||||||
# Install new/remove old packages
|
|
||||||
desired = manifest.enabled_packages()
|
|
||||||
desired_local = manifest.enabled_local_packages()
|
|
||||||
try:
|
|
||||||
state.packages = self.sync_packages(installer, state.packages, desired)
|
|
||||||
state.save()
|
|
||||||
state.local_packages = self.sync_local_packages(installer, source, state.local_packages, desired_local)
|
|
||||||
state.save()
|
|
||||||
except PackageError as e:
|
|
||||||
fatal(e)
|
|
||||||
|
|
||||||
# Run hooks
|
|
||||||
run_hooks(manifest, "post_update")
|
|
||||||
|
|
||||||
# Mark the new revision applied
|
|
||||||
state.applied_rev = tip
|
|
||||||
state.enabled_components = manifest.enabled_components
|
|
||||||
state.aur_helper = getattr(installer, "helper", state.aur_helper)
|
|
||||||
state.save()
|
|
||||||
|
|
||||||
self.summarize(changeset, new_files, revived_files)
|
|
||||||
|
|
||||||
def fetch_manifest(self, state: DotsState, applied_rev: str) -> tuple[DotsSource, str, Manifest]:
|
|
||||||
print()
|
|
||||||
log("Fetching dots repo...")
|
|
||||||
source = DotsSource()
|
|
||||||
try:
|
|
||||||
source.ensure()
|
|
||||||
tip = source.tip_rev()
|
|
||||||
if tip == applied_rev:
|
|
||||||
info("Dots already up to date.")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
manifest = source.manifest_at(tip)
|
|
||||||
if source.has_rev(applied_rev):
|
|
||||||
known = set(source.manifest_at(applied_rev).components)
|
|
||||||
else:
|
|
||||||
# Treat all components as known if rev is invalid so we don't overwrite existing prefs
|
|
||||||
known = set(manifest.components)
|
|
||||||
except (SourceError, ManifestError) as e:
|
|
||||||
fatal(e)
|
|
||||||
|
|
||||||
# Enable components recorded at install time + any new components that are default on
|
|
||||||
enabled = [
|
|
||||||
name
|
|
||||||
for name, comp in manifest.components.items()
|
|
||||||
if name in state.enabled_components or (name not in known and comp.default)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Let the user opt into any new optional components
|
|
||||||
new_comps = [name for name, comp in manifest.components.items() if name not in known and not comp.default]
|
|
||||||
if new_comps:
|
|
||||||
info(f"New components: {', '.join(new_comps)}")
|
|
||||||
enabled += prompt_selection(new_comps, "Components to enable?")
|
|
||||||
|
|
||||||
disabled = [name for name in manifest.components if name not in enabled]
|
|
||||||
try:
|
|
||||||
manifest.resolve_components(enable=enabled, disable=disabled)
|
|
||||||
except ComponentError as e:
|
|
||||||
fatal(e)
|
|
||||||
|
|
||||||
info(f"Enabled components: {', '.join(enabled) or 'none'}")
|
|
||||||
|
|
||||||
return source, tip, manifest
|
|
||||||
|
|
||||||
def deploy_changeset(
|
|
||||||
self, source: DotsSource, changeset: Changeset
|
|
||||||
) -> tuple[list[Path], list[Path], dict[str, str]]:
|
|
||||||
print()
|
|
||||||
|
|
||||||
if changeset.is_empty():
|
|
||||||
info("No configs to update.")
|
|
||||||
return [], [], {}
|
|
||||||
|
|
||||||
log("Updating configs...")
|
|
||||||
deployer = Deployer()
|
|
||||||
|
|
||||||
for repofile, dest in changeset.place:
|
|
||||||
src = source.working_path(repofile)
|
|
||||||
if not src.exists():
|
|
||||||
warn(f"missing in source, skipping: {repofile}")
|
|
||||||
continue
|
|
||||||
deployer.place_file(src, dest)
|
|
||||||
info(f"{repofile} -> {dest}")
|
|
||||||
|
|
||||||
new_files = []
|
|
||||||
for repofile, dest in changeset.conflicts:
|
|
||||||
src = source.working_path(repofile)
|
|
||||||
if not src.exists():
|
|
||||||
warn(f"missing in source, skipping: {repofile}")
|
|
||||||
continue
|
|
||||||
new_path = deployer.write_new(src, dest)
|
|
||||||
new_files.append(new_path)
|
|
||||||
warn(f"{dest} has local changes; upstream version written as {new_path.name}")
|
|
||||||
|
|
||||||
revived_files = []
|
|
||||||
for repofile, dest in changeset.deleted_changed:
|
|
||||||
src = source.working_path(repofile)
|
|
||||||
if not src.exists():
|
|
||||||
warn(f"missing in source, skipping: {repofile}")
|
|
||||||
continue
|
|
||||||
new_path = deployer.write_new(src, dest)
|
|
||||||
revived_files.append(new_path)
|
|
||||||
warn(f"{dest} was removed but changed upstream; upstream version written as {new_path.name}")
|
|
||||||
|
|
||||||
for dest in changeset.deletes:
|
|
||||||
deployer.remove(dest)
|
|
||||||
deployer.prune_empty_dirs(dest, Path.home())
|
|
||||||
info(f"Removed {dest}")
|
|
||||||
|
|
||||||
return new_files, revived_files, deployer.deployed_files
|
|
||||||
|
|
||||||
def sync_packages(self, installer: PackageInstaller, current: list[str], desired: list[str]) -> list[str]:
|
|
||||||
to_install = [p for p in desired if p not in current]
|
|
||||||
to_remove = [p for p in current if p not in desired]
|
|
||||||
installed = list(current)
|
|
||||||
|
|
||||||
if to_install:
|
|
||||||
print()
|
|
||||||
info(f"Installing new packages: {', '.join(to_install)}")
|
|
||||||
installer.install(to_install)
|
|
||||||
installed.extend(p for p in to_install if p not in installed)
|
|
||||||
|
|
||||||
if to_remove:
|
|
||||||
print()
|
|
||||||
info(f"Packages no longer required: {', '.join(to_remove)}")
|
|
||||||
selected = prompt_selection(to_remove, "Packages to remove?")
|
|
||||||
if selected:
|
|
||||||
installer.remove(selected)
|
|
||||||
installed = [p for p in installed if p not in selected]
|
|
||||||
|
|
||||||
return installed
|
|
||||||
|
|
||||||
def sync_local_packages(
|
|
||||||
self, installer: PackageInstaller, source: DotsSource, current: dict[str, list[str]], desired: list[str]
|
|
||||||
) -> dict[str, list[str]]:
|
|
||||||
to_build = [p for p in desired if p not in current]
|
|
||||||
to_rebuild = self.outdated_local_packages(installer, source, current, desired)
|
|
||||||
to_remove = [p for p in current if p not in desired]
|
|
||||||
installed = dict(current)
|
|
||||||
|
|
||||||
if to_build:
|
|
||||||
print()
|
|
||||||
log(f"Building new local packages: {', '.join(to_build)}")
|
|
||||||
installed.update(build_local_packages(installer, source, to_build))
|
|
||||||
|
|
||||||
if to_rebuild:
|
|
||||||
print()
|
|
||||||
log(f"Rebuilding updated local packages: {', '.join(to_rebuild)}")
|
|
||||||
installed.update(build_local_packages(installer, source, to_rebuild))
|
|
||||||
|
|
||||||
if to_remove:
|
|
||||||
print()
|
|
||||||
info(f"Local packages no longer required: {', '.join(to_remove)}")
|
|
||||||
selected = prompt_selection(to_remove, "Local packages to remove?")
|
|
||||||
if selected:
|
|
||||||
installer.remove([pkg for path in selected for pkg in current[path]])
|
|
||||||
for path in selected:
|
|
||||||
installed.pop(path, None)
|
|
||||||
|
|
||||||
return installed
|
|
||||||
|
|
||||||
def outdated_local_packages(
|
|
||||||
self, installer: PackageInstaller, source: DotsSource, current: dict[str, list[str]], desired: list[str]
|
|
||||||
) -> list[str]:
|
|
||||||
"""Repo paths whose installed packages are older than what the repo would build (skipped when off Arch)."""
|
|
||||||
|
|
||||||
outdated = []
|
|
||||||
for path in desired:
|
|
||||||
if path not in current:
|
|
||||||
continue
|
|
||||||
|
|
||||||
directory = source.working_path(path)
|
|
||||||
if not directory.is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
if installer.needs_rebuild(directory, current[path]):
|
|
||||||
outdated.append(path)
|
|
||||||
except PackageError as e:
|
|
||||||
# Failed to read PKGBUILD, leave it as-is
|
|
||||||
warn(f"could not check {path} for updates, leaving as-is: {e}")
|
|
||||||
|
|
||||||
return outdated
|
|
||||||
|
|
||||||
def summarize(self, changeset: Changeset, new_files: list[Path], revived_files: list[Path]) -> None:
|
|
||||||
print()
|
|
||||||
conflicts = len(new_files) + len(revived_files)
|
|
||||||
info(f"Updated {len(changeset.place)} file(s), removed {len(changeset.deletes)}, {conflicts} conflict(s).")
|
|
||||||
if new_files:
|
|
||||||
info("The following files were changed upstream but you had edited them locally.")
|
|
||||||
info("Your versions were kept; the upstream versions were written alongside as .new:")
|
|
||||||
for path in new_files:
|
|
||||||
info(f" {path}")
|
|
||||||
if revived_files:
|
|
||||||
info("These files were removed by you but changed upstream, so were not restored.")
|
|
||||||
info("The upstream versions were written alongside as .new:")
|
|
||||||
for path in revived_files:
|
|
||||||
info(f" {path}")
|
|
||||||
if changeset.stale:
|
|
||||||
info("These files are no longer managed but differ from what was installed, so were kept:")
|
|
||||||
for path in changeset.stale:
|
|
||||||
info(f" {path}")
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
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()}
|
|
||||||
@@ -11,7 +11,8 @@ def stddev(values: list[float], mean_val: float) -> float:
|
|||||||
return math.sqrt(sum((x - mean_val) ** 2 for x in values) / len(values)) if values else 0
|
return math.sqrt(sum((x - mean_val) ** 2 for x in values) / len(values)) if values else 0
|
||||||
|
|
||||||
|
|
||||||
def calc_colourfulness(image: Image.Image) -> float:
|
def calc_colourfulness(image: Image) -> float:
|
||||||
|
width, height = image.size
|
||||||
pixels = list(image.getdata()) # List of (R, G, B) tuples
|
pixels = list(image.getdata()) # List of (R, G, B) tuples
|
||||||
|
|
||||||
rg_diffs = []
|
rg_diffs = []
|
||||||
@@ -31,7 +32,7 @@ def calc_colourfulness(image: Image.Image) -> float:
|
|||||||
return math.sqrt(std_rg**2 + std_yb**2) + 0.3 * math.sqrt(mean_rg**2 + mean_yb**2)
|
return math.sqrt(std_rg**2 + std_yb**2) + 0.3 * math.sqrt(mean_rg**2 + mean_yb**2)
|
||||||
|
|
||||||
|
|
||||||
def get_variant(image: Image.Image) -> str:
|
def get_variant(image: Image) -> str:
|
||||||
colourfulness = calc_colourfulness(image)
|
colourfulness = calc_colourfulness(image)
|
||||||
|
|
||||||
if colourfulness < 10:
|
if colourfulness < 10:
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils.paths import cache_dir, config_dir, data_dir, dots_dir, state_dir
|
|
||||||
|
|
||||||
# Dirs to never prune even if empty
|
|
||||||
_PROTECTED_DIRS = frozenset({Path.home(), config_dir, data_dir, state_dir, cache_dir})
|
|
||||||
|
|
||||||
|
|
||||||
class Deployer:
|
|
||||||
"""Places files from the dots clone into their destinations."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.deployed_files: dict[str, str] = {}
|
|
||||||
|
|
||||||
def place(self, src: Path, dest: Path) -> None:
|
|
||||||
"""Place a whole entry (file or directory tree), replacing any existing dest."""
|
|
||||||
|
|
||||||
if src.is_dir():
|
|
||||||
self.place_dir(src, dest)
|
|
||||||
else:
|
|
||||||
self.place_file(src, dest)
|
|
||||||
|
|
||||||
def place_dir(self, src: Path, dest: Path) -> None:
|
|
||||||
"""Place a directory tree recursively, overwriting any existing dest files."""
|
|
||||||
|
|
||||||
if dest.is_symlink() or dest.is_file():
|
|
||||||
self.remove(dest)
|
|
||||||
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
for path in src.rglob("*"):
|
|
||||||
if path.is_file():
|
|
||||||
self.place_file(path, dest / path.relative_to(src))
|
|
||||||
elif path.is_dir():
|
|
||||||
(dest / path.relative_to(src)).mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
def place_file(self, src: Path, dest: Path, record: bool = True) -> None:
|
|
||||||
"""Atomically place a single file, replacing any existing dest."""
|
|
||||||
|
|
||||||
if dest.is_dir() and not dest.is_symlink():
|
|
||||||
self.remove(dest)
|
|
||||||
|
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
f = tempfile.NamedTemporaryFile(dir=dest.parent, delete=False)
|
|
||||||
f.close()
|
|
||||||
try:
|
|
||||||
shutil.copyfile(src, f.name)
|
|
||||||
shutil.copymode(src, f.name)
|
|
||||||
Path(f.name).replace(dest)
|
|
||||||
except BaseException:
|
|
||||||
Path(f.name).unlink()
|
|
||||||
raise
|
|
||||||
|
|
||||||
if record:
|
|
||||||
# Keep relative to dots dir
|
|
||||||
self.deployed_files[str(dest)] = str(src.relative_to(dots_dir))
|
|
||||||
|
|
||||||
def write_new(self, src: Path, dest: Path) -> Path:
|
|
||||||
"""Write the upstream version alongside dest as <dest>.new and return that path."""
|
|
||||||
|
|
||||||
new_path = dest.parent / f"{dest.name}.new"
|
|
||||||
self.place_file(src, new_path, record=False)
|
|
||||||
return new_path
|
|
||||||
|
|
||||||
def remove(self, path: Path) -> None:
|
|
||||||
if path.is_symlink() or path.is_file():
|
|
||||||
path.unlink()
|
|
||||||
elif path.is_dir():
|
|
||||||
shutil.rmtree(path)
|
|
||||||
|
|
||||||
def prune_empty_dirs(self, start: Path, stop: Path) -> None:
|
|
||||||
"""Removes dirs recursively from start to stop.
|
|
||||||
|
|
||||||
Will never prune protected dirs (home, config, cache, etc).
|
|
||||||
"""
|
|
||||||
|
|
||||||
parent = start.parent
|
|
||||||
while parent != stop and stop in parent.parents and parent not in _PROTECTED_DIRS:
|
|
||||||
try:
|
|
||||||
parent.rmdir()
|
|
||||||
except OSError:
|
|
||||||
break
|
|
||||||
parent = parent.parent
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils.dots.manifest import ManifestEntry
|
|
||||||
from caelestia.utils.dots.source import DotsSource, SourceError
|
|
||||||
from caelestia.utils.io import warn
|
|
||||||
|
|
||||||
|
|
||||||
class _Continue(Exception):
|
|
||||||
"""Signals the deployed-files loop to skip to the next entry."""
|
|
||||||
|
|
||||||
|
|
||||||
def _read_local(path: Path) -> bytes | None:
|
|
||||||
"""Read a local file, returning None if it can't be read (perms, is a dir, etc.)."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
return path.read_bytes()
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Changeset:
|
|
||||||
place: list[tuple[str, Path]] = field(default_factory=list) # (repofile, dest) to fast-forward
|
|
||||||
conflicts: list[tuple[str, Path]] = field(default_factory=list) # (repofile, dest) -> write .new
|
|
||||||
deletes: list[Path] = field(default_factory=list) # We placed it, upstream removed it, unmodified
|
|
||||||
stale: list[Path] = field(default_factory=list) # Upstream removed it but user modified it
|
|
||||||
deleted_changed: list[tuple[str, Path]] = field(default_factory=list) # User deleted it, upstream changed -> .new
|
|
||||||
untracked: list[Path] = field(default_factory=list) # Gone + no longer managed; drop from state
|
|
||||||
remap: list[tuple[str, Path]] = field(default_factory=list) # Up to date but source path moved; restate mapping
|
|
||||||
|
|
||||||
def is_empty(self) -> bool:
|
|
||||||
return not (self.place or self.conflicts or self.deletes or self.stale or self.deleted_changed)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def compute(
|
|
||||||
source: DotsSource,
|
|
||||||
applied_rev: str,
|
|
||||||
tip: str,
|
|
||||||
entries: list[ManifestEntry],
|
|
||||||
deployed: dict[str, str],
|
|
||||||
) -> "Changeset":
|
|
||||||
"""Collect all file changes needed into a Changeset."""
|
|
||||||
|
|
||||||
has_base = source.has_rev(applied_rev)
|
|
||||||
if not has_base:
|
|
||||||
warn(
|
|
||||||
"the previously applied revision is missing from the dots clone; files that differ "
|
|
||||||
"from the latest version will be written as .new instead of updated in place."
|
|
||||||
)
|
|
||||||
|
|
||||||
changed = set(source.changed_files(applied_rev, tip)) if has_base else set()
|
|
||||||
place: list[tuple[str, Path]] = []
|
|
||||||
conflicts: list[tuple[str, Path]] = []
|
|
||||||
deletes: list[Path] = []
|
|
||||||
stale: list[Path] = []
|
|
||||||
deleted_changed: list[tuple[str, Path]] = []
|
|
||||||
untracked: list[Path] = []
|
|
||||||
remap: list[tuple[str, Path]] = []
|
|
||||||
|
|
||||||
# Collect all files to deploy (entry sources can be dirs so we recurse into them)
|
|
||||||
to_deploy: dict[Path, str] = {}
|
|
||||||
for entry in entries:
|
|
||||||
src_root = str(entry.expanded_src())
|
|
||||||
repo_files = source.files_at(tip, src_root)
|
|
||||||
for dest in entry.expanded_dests():
|
|
||||||
for repo_file in repo_files:
|
|
||||||
to_deploy[dest / Path(repo_file).relative_to(src_root)] = repo_file
|
|
||||||
files_to_deploy = set(to_deploy)
|
|
||||||
|
|
||||||
# Already deployed files
|
|
||||||
for dest, src in deployed.items():
|
|
||||||
dest_path = Path(dest)
|
|
||||||
|
|
||||||
def try_read(rev: str, path: str) -> bytes:
|
|
||||||
try:
|
|
||||||
return source.blob_at(rev, path)
|
|
||||||
except SourceError:
|
|
||||||
# Read failed, keep it just in case
|
|
||||||
stale.append(dest_path)
|
|
||||||
raise _Continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
if dest_path not in files_to_deploy: # No longer managed by any entry
|
|
||||||
if not dest_path.exists():
|
|
||||||
# Gone from disk and no entry manages it
|
|
||||||
untracked.append(dest_path)
|
|
||||||
continue
|
|
||||||
|
|
||||||
local = _read_local(dest_path)
|
|
||||||
if local is not None and has_base and try_read(applied_rev, src) == local:
|
|
||||||
deletes.append(dest_path)
|
|
||||||
else:
|
|
||||||
# Modified, or unreadable so we can't verify; keep it just in case
|
|
||||||
stale.append(dest_path)
|
|
||||||
else: # Still managed; `src` is what we last placed, `new_src` the current source
|
|
||||||
new_src = to_deploy[dest_path]
|
|
||||||
if not dest_path.exists():
|
|
||||||
# User deleted a managed file locally
|
|
||||||
if has_base and new_src == src and new_src not in changed:
|
|
||||||
continue # Respect the deletion; upstream has nothing new to offer
|
|
||||||
# Upstream changed it (or base is unknown): surface as .new, don't restore
|
|
||||||
deleted_changed.append((new_src, dest_path))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if has_base and new_src == src and new_src not in changed:
|
|
||||||
continue # Unchanged upstream
|
|
||||||
|
|
||||||
dest_content = _read_local(dest_path)
|
|
||||||
if dest_content is None:
|
|
||||||
# Unreadable (perms, became a dir, ...); surface upstream as .new, don't clobber
|
|
||||||
conflicts.append((new_src, dest_path))
|
|
||||||
continue
|
|
||||||
|
|
||||||
if try_read(tip, new_src) == dest_content:
|
|
||||||
# Already up to date; restate the mapping if the source path moved
|
|
||||||
if new_src != src:
|
|
||||||
remap.append((new_src, dest_path))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Fast-forward only when the user hasn't edited since last deploy
|
|
||||||
if has_base and try_read(applied_rev, src) == dest_content:
|
|
||||||
place.append((new_src, dest_path))
|
|
||||||
else:
|
|
||||||
conflicts.append((new_src, dest_path))
|
|
||||||
except _Continue:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# New files to deploy
|
|
||||||
for dest in files_to_deploy - set(Path(d) for d in deployed):
|
|
||||||
src = to_deploy[dest]
|
|
||||||
try:
|
|
||||||
new_content = source.blob_at(tip, src)
|
|
||||||
except SourceError:
|
|
||||||
# Failed to read the upstream blob; skip rather than abort the whole update
|
|
||||||
warn(f"could not read from source, skipping: {src}")
|
|
||||||
continue
|
|
||||||
if not dest.exists() or new_content == _read_local(dest):
|
|
||||||
# Dest nonexistent or already equal to new content
|
|
||||||
place.append((src, dest))
|
|
||||||
else:
|
|
||||||
# Differs, or exists but unreadable; surface upstream as .new
|
|
||||||
conflicts.append((src, dest))
|
|
||||||
|
|
||||||
return Changeset(
|
|
||||||
place=place,
|
|
||||||
conflicts=conflicts,
|
|
||||||
deletes=deletes,
|
|
||||||
stale=stale,
|
|
||||||
deleted_changed=deleted_changed,
|
|
||||||
untracked=untracked,
|
|
||||||
remap=remap,
|
|
||||||
)
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils.paths import config_dir, data_dir
|
|
||||||
|
|
||||||
LEGACY_META_PKG = "caelestia-meta"
|
|
||||||
|
|
||||||
_confs = [
|
|
||||||
"hypr",
|
|
||||||
"starship.toml",
|
|
||||||
"foot",
|
|
||||||
"fish",
|
|
||||||
"fastfetch",
|
|
||||||
"uwsm",
|
|
||||||
"btop",
|
|
||||||
"spicetify",
|
|
||||||
"Code/User/settings.json",
|
|
||||||
"VSCodium/User/settings.json",
|
|
||||||
"Code/User/keybindings.json",
|
|
||||||
"VSCodium/User/keybindings.json",
|
|
||||||
"code-flags.conf",
|
|
||||||
"codium-flags.conf",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def _find_legacy_repo(path: Path) -> Path | None:
|
|
||||||
try:
|
|
||||||
remote = subprocess.check_output(["git", "-C", path, "remote", "get-url", "origin"], text=True)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check remote
|
|
||||||
if remote.strip() != "https://github.com/caelestia-dots/caelestia.git":
|
|
||||||
return
|
|
||||||
|
|
||||||
# Ignore anything outside home
|
|
||||||
if Path.home() not in path.parents:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Walk up parents (capped at home) to find the repo root
|
|
||||||
while path != Path.home() and not (path / ".git").is_dir():
|
|
||||||
path = path.parent
|
|
||||||
|
|
||||||
# Only return path if didn't hit home (we really don't want to nuke home)
|
|
||||||
if path != Path.home():
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_candidates(candidates: list[Path], legacy_dir: Path) -> list[Path]:
|
|
||||||
return [path for path in candidates if path.is_symlink() and legacy_dir in path.resolve().parents]
|
|
||||||
|
|
||||||
|
|
||||||
def detect_legacy_repo() -> Path | None:
|
|
||||||
for conf in _confs:
|
|
||||||
path = config_dir / conf
|
|
||||||
if not path.is_symlink():
|
|
||||||
continue
|
|
||||||
|
|
||||||
legacy_dir = _find_legacy_repo(path.resolve())
|
|
||||||
if legacy_dir:
|
|
||||||
return legacy_dir
|
|
||||||
|
|
||||||
return _find_legacy_repo(data_dir / "caelestia")
|
|
||||||
|
|
||||||
|
|
||||||
def legacy_config_symlinks(base: Path, legacy_dir: Path | None) -> list[Path]:
|
|
||||||
"""Config-relative links install.fish created, resolved under `base` (the live config or a backup of it)."""
|
|
||||||
|
|
||||||
if not legacy_dir:
|
|
||||||
return []
|
|
||||||
|
|
||||||
candidates = [base / conf for conf in _confs]
|
|
||||||
return _filter_candidates(candidates, legacy_dir)
|
|
||||||
|
|
||||||
|
|
||||||
def legacy_symlinks(legacy_dir: Path | None) -> list[Path]:
|
|
||||||
"""All paths symlinked into the legacy repo (the links install.fish created)."""
|
|
||||||
|
|
||||||
if not legacy_dir:
|
|
||||||
return []
|
|
||||||
|
|
||||||
extras = [
|
|
||||||
*(Path.home() / ".zen").glob("*/chrome/userChrome.css"),
|
|
||||||
Path.home() / ".local/lib/caelestia/caelestiafox",
|
|
||||||
]
|
|
||||||
|
|
||||||
return [*legacy_config_symlinks(config_dir, legacy_dir), *_filter_candidates(extras, legacy_dir)]
|
|
||||||
|
|
||||||
|
|
||||||
def legacy_to_delete(legacy_dir: Path | None) -> list[Path]:
|
|
||||||
if not legacy_dir:
|
|
||||||
return []
|
|
||||||
|
|
||||||
return [*legacy_symlinks(legacy_dir), legacy_dir]
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
import glob
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import tomllib
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from string import Template
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from caelestia.utils.io import warn
|
|
||||||
|
|
||||||
_XDG_DEFAULTS = {
|
|
||||||
"XDG_CONFIG_HOME": str(Path.home() / ".config"),
|
|
||||||
"XDG_DATA_HOME": str(Path.home() / ".local/share"),
|
|
||||||
"XDG_STATE_HOME": str(Path.home() / ".local/state"),
|
|
||||||
"XDG_CACHE_HOME": str(Path.home() / ".cache"),
|
|
||||||
}
|
|
||||||
_GLOB_MAGIC = re.compile(r"[*?[]")
|
|
||||||
_LOCAL_PREFIX = "local:"
|
|
||||||
|
|
||||||
|
|
||||||
class ManifestError(Exception):
|
|
||||||
"""Raised when manifest.toml is malformed."""
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentError(Exception):
|
|
||||||
"""Raised when component flags are invalid or contradictory."""
|
|
||||||
|
|
||||||
|
|
||||||
def _expand(text: str) -> Path:
|
|
||||||
"""Expand $VAR/${VAR} env vars (with XDG defaults) and ~ in a path."""
|
|
||||||
|
|
||||||
env = {**_XDG_DEFAULTS, **os.environ}
|
|
||||||
return Path(Template(text).safe_substitute(env)).expanduser()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ManifestEntry:
|
|
||||||
src: str
|
|
||||||
dest: str
|
|
||||||
|
|
||||||
def expanded_src(self) -> Path:
|
|
||||||
return _expand(self.src)
|
|
||||||
|
|
||||||
def expanded_dests(self) -> list[Path]:
|
|
||||||
"""The dest path with globs expanded.
|
|
||||||
|
|
||||||
Globs from the start until the segment with the last glob so subdirs are
|
|
||||||
created if they didn't exist previously.
|
|
||||||
"""
|
|
||||||
|
|
||||||
expanded = _expand(self.dest)
|
|
||||||
if not _GLOB_MAGIC.search(str(expanded)):
|
|
||||||
return [expanded]
|
|
||||||
|
|
||||||
parts = expanded.parts
|
|
||||||
glob_idx = max(i for i, part in enumerate(parts) if _GLOB_MAGIC.search(part))
|
|
||||||
pattern = str(Path(*parts[: glob_idx + 1]))
|
|
||||||
tail = parts[glob_idx + 1 :]
|
|
||||||
matches = sorted(glob.glob(pattern))
|
|
||||||
if tail: # Only match dirs if a tail exists
|
|
||||||
matches = [match for match in matches if Path(match).is_dir()]
|
|
||||||
return [Path(match, *tail) for match in matches]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ManifestComponent:
|
|
||||||
name: str
|
|
||||||
default: bool = False
|
|
||||||
packages: list[str] = field(default_factory=list)
|
|
||||||
entries: list[ManifestEntry] = field(default_factory=list)
|
|
||||||
post_package: list[str] = field(default_factory=list)
|
|
||||||
post_install: list[str] = field(default_factory=list)
|
|
||||||
post_update: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class _ManifestData:
|
|
||||||
enabled_comps: list[str] = field(default_factory=list)
|
|
||||||
disabled_comps: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Manifest:
|
|
||||||
components: dict[str, ManifestComponent] = field(default_factory=dict)
|
|
||||||
packages: list[str] = field(default_factory=list)
|
|
||||||
post_package: list[str] = field(default_factory=list) # Post package install (install cmd only)
|
|
||||||
post_install: list[str] = field(default_factory=list) # Very end of install cmd
|
|
||||||
post_update: list[str] = field(default_factory=list) # Very end of update cmd
|
|
||||||
_data: _ManifestData = field(default_factory=_ManifestData, init=False, repr=False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def enabled_components(self) -> list[str]:
|
|
||||||
return self._data.enabled_comps
|
|
||||||
|
|
||||||
@property
|
|
||||||
def disabled_components(self) -> list[str]:
|
|
||||||
return self._data.disabled_comps
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse(text: str) -> "Manifest":
|
|
||||||
try:
|
|
||||||
raw = tomllib.loads(text)
|
|
||||||
except tomllib.TOMLDecodeError as e:
|
|
||||||
raise ManifestError(f"invalid TOML: {e}") from e
|
|
||||||
|
|
||||||
post_package = _validate_str_list(raw.get("post_package", []), "post_package")
|
|
||||||
post_install = _validate_str_list(raw.get("post_install", []), "post_install")
|
|
||||||
post_update = _validate_str_list(raw.get("post_update", []), "post_update")
|
|
||||||
|
|
||||||
packages = _validate_str_list(raw.get("packages", []), "packages")
|
|
||||||
|
|
||||||
components = {}
|
|
||||||
for comp in raw.get("components", []):
|
|
||||||
parsed = _parse_component(comp)
|
|
||||||
if parsed.name in components:
|
|
||||||
warn(f"duplicate component '{parsed.name}'; using the last definition")
|
|
||||||
components[parsed.name] = parsed
|
|
||||||
|
|
||||||
return Manifest(
|
|
||||||
components=components,
|
|
||||||
packages=packages,
|
|
||||||
post_package=post_package,
|
|
||||||
post_install=post_install,
|
|
||||||
post_update=post_update,
|
|
||||||
)
|
|
||||||
|
|
||||||
def resolve_components(
|
|
||||||
self,
|
|
||||||
enable: list[str] | None = None,
|
|
||||||
disable: list[str] | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Resolves enabled/disabled components. This MUST be called before calling any other method."""
|
|
||||||
|
|
||||||
enable_set = set(enable or [])
|
|
||||||
disable_set = set(disable or [])
|
|
||||||
known = set(self.components)
|
|
||||||
|
|
||||||
for name in enable_set | disable_set:
|
|
||||||
if name not in known:
|
|
||||||
raise ComponentError(f"unknown component: {name}")
|
|
||||||
|
|
||||||
conflict = enable_set & disable_set
|
|
||||||
if conflict:
|
|
||||||
raise ComponentError(f"component(s) both enabled and disabled: {', '.join(sorted(conflict))}")
|
|
||||||
|
|
||||||
enabled = {name for name, comp in self.components.items() if comp.default}
|
|
||||||
enabled |= enable_set
|
|
||||||
enabled -= disable_set
|
|
||||||
|
|
||||||
self._data.enabled_comps.clear()
|
|
||||||
self._data.disabled_comps.clear()
|
|
||||||
for name in self.components:
|
|
||||||
if name in enabled:
|
|
||||||
self._data.enabled_comps.append(name)
|
|
||||||
else:
|
|
||||||
self._data.disabled_comps.append(name)
|
|
||||||
|
|
||||||
def enabled_entries(self) -> list[ManifestEntry]:
|
|
||||||
"""The entries of every enabled component."""
|
|
||||||
|
|
||||||
entries: list[ManifestEntry] = []
|
|
||||||
for name in self._data.enabled_comps:
|
|
||||||
entries.extend(self.components[name].entries)
|
|
||||||
return entries
|
|
||||||
|
|
||||||
def enabled_hooks(self, kind: str) -> list[str]:
|
|
||||||
"""Global + enabled components' hooks of the given kind."""
|
|
||||||
|
|
||||||
hooks = list(getattr(self, kind))
|
|
||||||
for name in self._data.enabled_comps:
|
|
||||||
hooks.extend(getattr(self.components[name], kind))
|
|
||||||
return hooks
|
|
||||||
|
|
||||||
def enabled_packages(self) -> list[str]:
|
|
||||||
"""Repo/AUR packages to install."""
|
|
||||||
return [p for p in self._all_packages() if not p.startswith(_LOCAL_PREFIX)]
|
|
||||||
|
|
||||||
def enabled_local_packages(self) -> list[str]:
|
|
||||||
"""Local PKGBUILD dirs to build.
|
|
||||||
|
|
||||||
Local packages are determined by a local: prefix and are
|
|
||||||
relative dirs instead of package names.
|
|
||||||
"""
|
|
||||||
return [p[len(_LOCAL_PREFIX) :] for p in self._all_packages() if p.startswith(_LOCAL_PREFIX)]
|
|
||||||
|
|
||||||
def _all_packages(self) -> list[str]:
|
|
||||||
"""The manifest's top-level packages plus enabled components', in manifest order.
|
|
||||||
|
|
||||||
Top-level packages come first, then each enabled component's packages in
|
|
||||||
component order. Only the first occurrence of each package is kept.
|
|
||||||
"""
|
|
||||||
|
|
||||||
seen: set[str] = set()
|
|
||||||
ordered: list[str] = []
|
|
||||||
for pkg in (*self.packages, *(p for c in self._data.enabled_comps for p in self.components[c].packages)):
|
|
||||||
if pkg not in seen:
|
|
||||||
seen.add(pkg)
|
|
||||||
ordered.append(pkg)
|
|
||||||
return ordered
|
|
||||||
|
|
||||||
|
|
||||||
def _require_key(d: dict[str, Any], key: str, ctx: str) -> Any:
|
|
||||||
if key not in d:
|
|
||||||
raise ManifestError(f"{ctx}: missing required key '{key}'")
|
|
||||||
return d[key]
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_str_list(value: Any, ctx: str) -> list[str]:
|
|
||||||
if not isinstance(value, list) or not all(isinstance(v, str) for v in value):
|
|
||||||
raise ManifestError(f"{ctx}: expected a list of strings")
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_entry(d: Any) -> ManifestEntry:
|
|
||||||
if not isinstance(d, dict):
|
|
||||||
raise ManifestError("entry: expected a table")
|
|
||||||
return ManifestEntry(src=_require_key(d, "src", "entry"), dest=_require_key(d, "dest", "entry"))
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_component(d: dict[str, Any]) -> ManifestComponent:
|
|
||||||
name = _require_key(d, "name", "component")
|
|
||||||
return ManifestComponent(
|
|
||||||
name=name,
|
|
||||||
default=bool(d.get("default", False)),
|
|
||||||
packages=_validate_str_list(d.get("packages", []), f"component '{name}' packages"),
|
|
||||||
entries=[_parse_entry(e) for e in d.get("entries", [])],
|
|
||||||
post_package=_validate_str_list(d.get("post_package", []), f"component '{name}' post_package"),
|
|
||||||
post_install=_validate_str_list(d.get("post_install", []), f"component '{name}' post_install"),
|
|
||||||
post_update=_validate_str_list(d.get("post_update", []), f"component '{name}' post_update"),
|
|
||||||
)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
from caelestia.utils.dots.manifest import Manifest
|
|
||||||
from caelestia.utils.dots.packages import PackageInstaller
|
|
||||||
from caelestia.utils.dots.source import DotsSource
|
|
||||||
from caelestia.utils.io import info, log, warn
|
|
||||||
from caelestia.utils.paths import dots_dir
|
|
||||||
|
|
||||||
|
|
||||||
def build_local_packages(installer: PackageInstaller, source: DotsSource, paths: list[str]) -> dict[str, list[str]]:
|
|
||||||
"""Build and install each local PKGBUILD dir, returning {path: installed package names}."""
|
|
||||||
|
|
||||||
built: dict[str, list[str]] = {}
|
|
||||||
for path in paths:
|
|
||||||
directory = source.working_path(path)
|
|
||||||
if not directory.is_dir():
|
|
||||||
warn(f"missing in repo, skipping: {path}")
|
|
||||||
continue
|
|
||||||
log(f"Building {path}...")
|
|
||||||
built[path] = installer.build_install(directory)
|
|
||||||
return built
|
|
||||||
|
|
||||||
|
|
||||||
def run_hooks(manifest: Manifest, kind: str) -> None:
|
|
||||||
"""Run the global + enabled components' hooks of the given kind (e.g. "post_install")."""
|
|
||||||
|
|
||||||
hooks = manifest.enabled_hooks(kind)
|
|
||||||
if not hooks:
|
|
||||||
return
|
|
||||||
|
|
||||||
print()
|
|
||||||
log(f"Running {kind.replace('_', '-')} hooks...")
|
|
||||||
env = {**os.environ, "CAELESTIA_DOTS": str(dots_dir)}
|
|
||||||
for hook in hooks:
|
|
||||||
info(f"Running hook: {hook}")
|
|
||||||
result = subprocess.run(hook, shell=True, env=env)
|
|
||||||
if result.returncode != 0:
|
|
||||||
warn(f"hook exited with {result.returncode}")
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils.io import fatal, info, warn
|
|
||||||
|
|
||||||
DEFAULT_AUR_HELPER = "paru"
|
|
||||||
AUR_HELPERS = DEFAULT_AUR_HELPER, "yay"
|
|
||||||
|
|
||||||
|
|
||||||
class PackageError(Exception):
|
|
||||||
"""Raised when a package operation (install/remove/build/update) fails."""
|
|
||||||
|
|
||||||
|
|
||||||
def _try_run(cmd: list[str], error_msg: str, **kwargs) -> None:
|
|
||||||
"""Run a subprocess, raising `PackageError` if it fails."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.run(cmd, check=True, **kwargs)
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
||||||
raise PackageError(error_msg) from e
|
|
||||||
|
|
||||||
|
|
||||||
def _read_srcinfo(directory: Path) -> dict[str, list[str]]:
|
|
||||||
"""Run `makepkg --printsrcinfo` in `directory`, grouping each key to its list of values."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
srcinfo = subprocess.check_output(["makepkg", "--printsrcinfo"], cwd=directory, text=True)
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
||||||
raise PackageError(f"failed to read package metadata in {directory}") from e
|
|
||||||
|
|
||||||
fields: dict[str, list[str]] = {}
|
|
||||||
for line in srcinfo.splitlines():
|
|
||||||
key, sep, value = line.partition("=")
|
|
||||||
if not sep:
|
|
||||||
continue
|
|
||||||
fields.setdefault(key.strip(), []).append(value.strip())
|
|
||||||
return fields
|
|
||||||
|
|
||||||
|
|
||||||
def _srcinfo_version(fields: dict[str, list[str]]) -> str | None:
|
|
||||||
"""Build the `[epoch:]pkgver-pkgrel` version string from parsed .SRCINFO fields, or None if absent."""
|
|
||||||
|
|
||||||
pkgver = next(iter(fields.get("pkgver", [])), None)
|
|
||||||
pkgrel = next(iter(fields.get("pkgrel", [])), None)
|
|
||||||
if pkgver is None or pkgrel is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
version = f"{pkgver}-{pkgrel}"
|
|
||||||
epoch = next(iter(fields.get("epoch", [])), None)
|
|
||||||
return f"{epoch}:{version}" if epoch else version
|
|
||||||
|
|
||||||
|
|
||||||
def _vercmp(a: str, b: str) -> int:
|
|
||||||
"""Use pacman's `vercmp` to compare to package versions."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
return int(subprocess.check_output(["vercmp", a, b], text=True).strip())
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, ValueError) as e:
|
|
||||||
warn(f"vercmp failed, assuming equal: {e}")
|
|
||||||
return 0 # Don't rebuild when unable to check version
|
|
||||||
|
|
||||||
|
|
||||||
def _install_aur_helper(helper: str, noconfirm: bool = False) -> None:
|
|
||||||
pacman_cmd = ["sudo", "pacman", "-S", "--needed", "git", "base-devel"]
|
|
||||||
if noconfirm:
|
|
||||||
pacman_cmd.append("--noconfirm")
|
|
||||||
_try_run(pacman_cmd, "failed to install AUR helper build dependencies")
|
|
||||||
|
|
||||||
repo_url = f"https://aur.archlinux.org/{helper}.git"
|
|
||||||
with tempfile.TemporaryDirectory() as repo_dir:
|
|
||||||
_try_run(["git", "clone", repo_url, repo_dir], f"failed to clone {helper} from the AUR")
|
|
||||||
|
|
||||||
makepkg_cmd = ["makepkg", "-si"]
|
|
||||||
if noconfirm:
|
|
||||||
makepkg_cmd.append("--noconfirm")
|
|
||||||
_try_run(makepkg_cmd, f"failed to build and install {helper}", cwd=repo_dir)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if helper == "yay":
|
|
||||||
subprocess.run(["yay", "-Y", "--gendb"], check=True)
|
|
||||||
subprocess.run(["yay", "-Y", "--devel", "--save"], check=True)
|
|
||||||
elif helper == "paru":
|
|
||||||
subprocess.run(["paru", "--gendb"], check=True)
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
|
||||||
warn(f"failed to run AUR helper post install actions: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
class PackageInstaller(ABC):
|
|
||||||
@staticmethod
|
|
||||||
def get(helper: str | None = None, noconfirm: bool = False) -> "PackageInstaller":
|
|
||||||
"""Pick a package installer: the requested/detected AUR helper on Arch, else a no-op."""
|
|
||||||
|
|
||||||
# Not on Arch, can't install packages
|
|
||||||
if shutil.which("pacman") is None:
|
|
||||||
return NoopInstaller()
|
|
||||||
|
|
||||||
# Explicitly given
|
|
||||||
if helper:
|
|
||||||
if not shutil.which(helper):
|
|
||||||
if helper not in AUR_HELPERS:
|
|
||||||
fatal(f"given AUR helper {helper} is not installed and is unable to be installed automatically.")
|
|
||||||
|
|
||||||
info(f"Given AUR helper not installed. Installing {helper}...")
|
|
||||||
_install_aur_helper(helper, noconfirm)
|
|
||||||
return ArchInstaller(helper, noconfirm)
|
|
||||||
|
|
||||||
# Not given, find installed one
|
|
||||||
for candidate in AUR_HELPERS:
|
|
||||||
if shutil.which(candidate):
|
|
||||||
return ArchInstaller(candidate, noconfirm)
|
|
||||||
|
|
||||||
info(f"No AUR helper found. Installing {DEFAULT_AUR_HELPER}...")
|
|
||||||
_install_aur_helper(DEFAULT_AUR_HELPER, noconfirm)
|
|
||||||
return ArchInstaller(DEFAULT_AUR_HELPER, noconfirm)
|
|
||||||
|
|
||||||
# --- Abstract methods ---
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def install(self, packages: list[str]) -> None: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def remove(self, packages: list[str]) -> None: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def build_install(self, directory: Path) -> list[str]:
|
|
||||||
"""Build and install the PKGBUILD in `directory`, returning the installed package names."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def installed_version(self, package: str) -> str | None:
|
|
||||||
"""Return the installed version of `package`, or None if it is not installed."""
|
|
||||||
|
|
||||||
def is_installed(self, package: str) -> bool:
|
|
||||||
return self.installed_version(package) is not None
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def needs_rebuild(self, directory: Path, packages: list[str]) -> bool:
|
|
||||||
"""Whether the PKGBUILD in `directory` would build a version differing from the installed `packages`."""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def system_update(self) -> None: ...
|
|
||||||
|
|
||||||
|
|
||||||
class NoopInstaller(PackageInstaller):
|
|
||||||
"""Used off Arch, where the dots' packages are not available via pacman/AUR."""
|
|
||||||
|
|
||||||
def install(self, packages: list[str]) -> None:
|
|
||||||
if packages:
|
|
||||||
info(f"Skipping package install (not on Arch): {', '.join(packages)}")
|
|
||||||
|
|
||||||
def remove(self, packages: list[str]) -> None:
|
|
||||||
if packages:
|
|
||||||
info(f"Skipping package removal (not on Arch): {', '.join(packages)}")
|
|
||||||
|
|
||||||
def build_install(self, directory: Path) -> list[str]:
|
|
||||||
info(f"Skipping local package build (not on Arch): {directory}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def installed_version(self, package: str) -> str | None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def needs_rebuild(self, directory: Path, packages: list[str]) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def system_update(self) -> None:
|
|
||||||
info("Skipping system update (not on Arch)")
|
|
||||||
|
|
||||||
|
|
||||||
class ArchInstaller(PackageInstaller):
|
|
||||||
def __init__(self, helper: str, noconfirm: bool = False) -> None:
|
|
||||||
self.helper = helper
|
|
||||||
self.flags = ["--noconfirm"] if noconfirm else []
|
|
||||||
|
|
||||||
def install(self, packages: list[str], explicit: bool = True) -> None:
|
|
||||||
if not packages:
|
|
||||||
return
|
|
||||||
|
|
||||||
cmd = [self.helper, "-S", "--needed", *self.flags]
|
|
||||||
if not explicit:
|
|
||||||
cmd.append("--asdeps") # Set install reason to dep (does not affect already installed packages)
|
|
||||||
_try_run(cmd + packages, f"failed to install packages: {', '.join(packages)}")
|
|
||||||
|
|
||||||
# Force install reason to explicit install
|
|
||||||
if explicit:
|
|
||||||
# `-D` only accepts real installed names, so resolve any virtual/`provides` names (e.g. awk -> gawk)
|
|
||||||
resolved = [self._installed_name(pkg) for pkg in packages]
|
|
||||||
try:
|
|
||||||
subprocess.run([self.helper, "-D", "--asexplicit", *self.flags, *resolved], check=True)
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
||||||
warn(f"failed to mark packages as explicitly installed: {', '.join(resolved)}")
|
|
||||||
|
|
||||||
def remove(self, packages: list[str]) -> None:
|
|
||||||
if not packages:
|
|
||||||
return
|
|
||||||
_try_run([self.helper, "-Rns", *self.flags, *packages], f"failed to remove packages: {', '.join(packages)}")
|
|
||||||
|
|
||||||
def build_install(self, directory: Path) -> list[str]:
|
|
||||||
fields = _read_srcinfo(directory)
|
|
||||||
names = fields.get("pkgname", [])
|
|
||||||
depends = fields.get("depends", [])
|
|
||||||
|
|
||||||
self.install(depends, explicit=False)
|
|
||||||
|
|
||||||
# Stop makepkg from resetting sudo
|
|
||||||
env = {**os.environ, "PACMAN_AUTH": "sudo"}
|
|
||||||
# -f = force, -s = sync deps, -i = install
|
|
||||||
_try_run(
|
|
||||||
["makepkg", "-fsi", *self.flags], f"failed to build local package in {directory}", cwd=directory, env=env
|
|
||||||
)
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
for artifact in directory.glob("*.pkg.tar*"):
|
|
||||||
try:
|
|
||||||
artifact.unlink()
|
|
||||||
except OSError as e:
|
|
||||||
warn(f"failed to remove build artifact {artifact}: {e}")
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def _query(self, package: str) -> tuple[str, str] | None:
|
|
||||||
"""Return the installed (name, version) of `package`, resolving `provides` (e.g. awk -> gawk), or None."""
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
["pacman", "-Q", package],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return None
|
|
||||||
# `pacman -Q` resolves provides and prints "<real name> <version>"
|
|
||||||
parts = result.stdout.split()
|
|
||||||
return (parts[0], parts[1]) if len(parts) >= 2 else None
|
|
||||||
|
|
||||||
def _installed_name(self, package: str) -> str:
|
|
||||||
"""Resolve `package` to its real installed name (handles provides), falling back to the given name."""
|
|
||||||
|
|
||||||
query = self._query(package)
|
|
||||||
return query[0] if query else package
|
|
||||||
|
|
||||||
def installed_version(self, package: str) -> str | None:
|
|
||||||
query = self._query(package)
|
|
||||||
return query[1] if query else None
|
|
||||||
|
|
||||||
def needs_rebuild(self, directory: Path, packages: list[str]) -> bool:
|
|
||||||
built = _srcinfo_version(_read_srcinfo(directory))
|
|
||||||
if built is None:
|
|
||||||
return False # Can't determine the source version, leave as is
|
|
||||||
|
|
||||||
# Rebuild when installed version < repo version
|
|
||||||
# Don't rebuild packages that have been removed
|
|
||||||
return any(
|
|
||||||
(installed := self.installed_version(pkg)) is not None and _vercmp(built, installed) > 0 for pkg in packages
|
|
||||||
)
|
|
||||||
|
|
||||||
def system_update(self) -> None:
|
|
||||||
_try_run([self.helper, "-Syu", *self.flags], "failed to perform system update")
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from caelestia.utils.dots.manifest import Manifest
|
|
||||||
from caelestia.utils.paths import dots_dir, get_config
|
|
||||||
|
|
||||||
|
|
||||||
class SourceError(Exception):
|
|
||||||
"""Raised when a git operation against the dots clone fails."""
|
|
||||||
|
|
||||||
|
|
||||||
class DotsSource:
|
|
||||||
_fetched_source: bool = False
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
cfg = get_config().get("dots", {})
|
|
||||||
self.url = cfg.get("url", "https://github.com/caelestia-dots/caelestia.git")
|
|
||||||
self.branch = cfg.get("branch", "main")
|
|
||||||
# Cache git blobs by (ref, relpath); objects are immutable for a given rev
|
|
||||||
self._blob_cache: dict[tuple[str, str], bytes] = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def remote_ref(self) -> str:
|
|
||||||
return f"origin/{self.branch}"
|
|
||||||
|
|
||||||
def exists(self) -> bool:
|
|
||||||
return (dots_dir / ".git").is_dir()
|
|
||||||
|
|
||||||
def working_path(self, relpath: str | Path) -> Path:
|
|
||||||
"""Get a Path relative to the dots dir."""
|
|
||||||
return dots_dir / relpath
|
|
||||||
|
|
||||||
def ensure(self) -> None:
|
|
||||||
"""Clone the repo if absent, otherwise fetch the latest refs.
|
|
||||||
|
|
||||||
If the configured url changed, the stale clone is removed and re-cloned
|
|
||||||
from the new source.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.exists():
|
|
||||||
if self.current_url() == self.url:
|
|
||||||
if DotsSource._fetched_source:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._git("fetch", "--prune", "origin", self.branch)
|
|
||||||
DotsSource._fetched_source = True
|
|
||||||
return
|
|
||||||
shutil.rmtree(dots_dir)
|
|
||||||
|
|
||||||
dots_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
self._run("git", "clone", "--branch", self.branch, self.url, str(dots_dir))
|
|
||||||
DotsSource._fetched_source = True
|
|
||||||
|
|
||||||
def current_url(self) -> str:
|
|
||||||
return self._git("remote", "get-url", "origin").strip()
|
|
||||||
|
|
||||||
def checkout_tip(self) -> str:
|
|
||||||
"""Reset the working tree to the fetched tip and return its commit hash."""
|
|
||||||
|
|
||||||
self._git("reset", "--hard", self.remote_ref)
|
|
||||||
return self.tip_rev()
|
|
||||||
|
|
||||||
def tip_rev(self) -> str:
|
|
||||||
return self._git("rev-parse", self.remote_ref).strip()
|
|
||||||
|
|
||||||
def changed_files(self, base: str, head: str) -> list[str]:
|
|
||||||
"""Repo-relative paths that differ between two revisions."""
|
|
||||||
|
|
||||||
out = self._git("diff", "--name-only", base, head)
|
|
||||||
return [line for line in out.splitlines() if line]
|
|
||||||
|
|
||||||
def has_rev(self, rev: str) -> bool:
|
|
||||||
"""Whether `rev` resolves to a commit."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._git("rev-parse", "--verify", "--quiet", f"{rev}^{{commit}}")
|
|
||||||
return True
|
|
||||||
except SourceError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def clean(self) -> None:
|
|
||||||
"""Remove all untracked files in the git repo."""
|
|
||||||
self._git("clean", "-fdx")
|
|
||||||
|
|
||||||
# --- Accessors ---
|
|
||||||
|
|
||||||
def manifest_at(self, ref: str) -> Manifest:
|
|
||||||
return Manifest.parse(self.text_at(ref, "manifest.toml"))
|
|
||||||
|
|
||||||
def text_at(self, ref: str, relpath: str) -> str:
|
|
||||||
return self._git("show", f"{ref}:{relpath}")
|
|
||||||
|
|
||||||
def blob_at(self, ref: str, relpath: str) -> bytes:
|
|
||||||
key = (ref, relpath)
|
|
||||||
if key not in self._blob_cache:
|
|
||||||
self._blob_cache[key] = self._git_bytes("show", f"{ref}:{relpath}")
|
|
||||||
return self._blob_cache[key]
|
|
||||||
|
|
||||||
def files_at(self, ref: str, relpath: str) -> list[str]:
|
|
||||||
"""Repo-relative paths of all files under relpath at ref (the path itself if it is a file)."""
|
|
||||||
|
|
||||||
out = self._git("ls-tree", "-r", "--name-only", ref, "--", relpath)
|
|
||||||
return [line for line in out.splitlines() if line]
|
|
||||||
|
|
||||||
# --- Helpers ---
|
|
||||||
|
|
||||||
def _git(self, *args: str) -> str:
|
|
||||||
# core.quotePath=false so non-ASCII paths come back verbatim, not octal-escaped
|
|
||||||
return self._run("git", "-C", str(dots_dir), "-c", "core.quotePath=false", *args)
|
|
||||||
|
|
||||||
def _git_bytes(self, *args: str) -> bytes:
|
|
||||||
cmd = ["git", "-C", str(dots_dir), "-c", "core.quotePath=false", *args]
|
|
||||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise SourceError(result.stderr.decode().strip() or f"git {' '.join(args)} failed")
|
|
||||||
return result.stdout
|
|
||||||
|
|
||||||
def _run(self, *cmd: str) -> str:
|
|
||||||
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise SourceError(result.stderr.strip() or f"{' '.join(cmd)} failed")
|
|
||||||
return result.stdout
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import json
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
from caelestia.utils.dots.packages import DEFAULT_AUR_HELPER
|
|
||||||
from caelestia.utils.io import warn
|
|
||||||
from caelestia.utils.paths import atomic_dump, dots_state_path
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DotsState:
|
|
||||||
# The AUR helper selected selected at install time
|
|
||||||
aur_helper: str = "paru"
|
|
||||||
|
|
||||||
# The git rev of currently applied dots version
|
|
||||||
applied_rev: str | None = None
|
|
||||||
|
|
||||||
# The currently enabled components
|
|
||||||
enabled_components: list[str] = field(default_factory=list)
|
|
||||||
|
|
||||||
# Previously installed packages/local packages
|
|
||||||
packages: list[str] = field(default_factory=list)
|
|
||||||
local_packages: dict[str, list[str]] = field(default_factory=dict)
|
|
||||||
|
|
||||||
# Files placed by the last deploy. Only files, not directories
|
|
||||||
# Maps dest -> src
|
|
||||||
deployed_files: dict[str, str] = field(default_factory=dict)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load() -> "DotsState":
|
|
||||||
try:
|
|
||||||
data = json.loads(dots_state_path.read_text())
|
|
||||||
except FileNotFoundError:
|
|
||||||
return DotsState()
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
warn("failed to parse current dots state.")
|
|
||||||
return DotsState()
|
|
||||||
|
|
||||||
return DotsState(
|
|
||||||
aur_helper=data.get("aur_helper", DEFAULT_AUR_HELPER),
|
|
||||||
applied_rev=data.get("applied_rev"),
|
|
||||||
enabled_components=data.get("enabled_components", []),
|
|
||||||
packages=data.get("packages", []),
|
|
||||||
local_packages=data.get("local_packages", {}),
|
|
||||||
deployed_files=data.get("deployed_files", {}),
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self) -> None:
|
|
||||||
atomic_dump(
|
|
||||||
dots_state_path,
|
|
||||||
{
|
|
||||||
"aur_helper": self.aur_helper,
|
|
||||||
"applied_rev": self.applied_rev,
|
|
||||||
"enabled_components": self.enabled_components,
|
|
||||||
"packages": self.packages,
|
|
||||||
"local_packages": self.local_packages,
|
|
||||||
"deployed_files": self.deployed_files,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
import json
|
import json as j
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}"
|
socket_base = f"{os.getenv('XDG_RUNTIME_DIR')}/hypr/{os.getenv('HYPRLAND_INSTANCE_SIGNATURE')}"
|
||||||
socket_path = f"{socket_base}/.socket.sock"
|
socket_path = f"{socket_base}/.socket.sock"
|
||||||
socket2_path = f"{socket_base}/.socket2.sock"
|
socket2_path = f"{socket_base}/.socket2.sock"
|
||||||
|
|
||||||
_lua_config_cache: bool | None = None
|
|
||||||
|
|
||||||
def message(msg: str, is_json: bool = True) -> str | dict[str, Any]:
|
def message(msg: str, json: bool = True) -> str | dict[str, any]:
|
||||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
||||||
sock.connect(socket_path)
|
sock.connect(socket_path)
|
||||||
|
|
||||||
if is_json:
|
if json:
|
||||||
msg = f"j/{msg}"
|
msg = f"j/{msg}"
|
||||||
sock.send(msg.encode())
|
sock.send(msg.encode())
|
||||||
|
|
||||||
@@ -24,46 +22,8 @@ def message(msg: str, is_json: bool = True) -> str | dict[str, Any]:
|
|||||||
break
|
break
|
||||||
resp += new_resp.decode()
|
resp += new_resp.decode()
|
||||||
|
|
||||||
return json.loads(resp) if is_json else resp
|
return j.loads(resp) if json else resp
|
||||||
|
|
||||||
|
|
||||||
def is_lua_config() -> bool:
|
def dispatch(dispatcher: str, *args: list[any]) -> bool:
|
||||||
global _lua_config_cache
|
return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), json=False) == "ok"
|
||||||
if _lua_config_cache is not None:
|
|
||||||
return _lua_config_cache
|
|
||||||
try:
|
|
||||||
result = message("systeminfo", is_json=False)
|
|
||||||
for line in result.splitlines():
|
|
||||||
if "configProvider:" in line:
|
|
||||||
_lua_config_cache = "lua" in line.lower()
|
|
||||||
return _lua_config_cache
|
|
||||||
_lua_config_cache = False
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
_lua_config_cache = False
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
DISPATCHER_MAP_LUA = {
|
|
||||||
"togglespecialworkspace": lambda *a: f'hl.dsp.workspace.toggle_special("{a[0]}")' if a else 'hl.dsp.workspace.toggle_special()',
|
|
||||||
"movetoworkspacesilent": lambda *a: (
|
|
||||||
f'hl.dsp.window.move({{window = "address:{a[0].split(",")[1].replace("address:", "")}", workspace = "{a[0].split(",")[0]}", follow = false}})'
|
|
||||||
),
|
|
||||||
"exec": lambda *a: 'hl.dsp.exec_cmd("' + ' '.join(a).replace('\\', '\\\\').replace('"', '\\"') + '")',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def dispatch(dispatcher: str, *args: str) -> bool:
|
|
||||||
if is_lua_config() and dispatcher in DISPATCHER_MAP_LUA:
|
|
||||||
lua_dispatch = DISPATCHER_MAP_LUA[dispatcher](*args)
|
|
||||||
return message(f"dispatch {lua_dispatch}", is_json=False) == "ok"
|
|
||||||
return message(f"dispatch {dispatcher} {' '.join(map(str, args))}".rstrip(), is_json=False) == "ok"
|
|
||||||
|
|
||||||
|
|
||||||
def batch(*msgs: str, is_json: bool = False) -> str | dict[str, Any]:
|
|
||||||
formatted_msgs = msgs
|
|
||||||
|
|
||||||
if is_json:
|
|
||||||
formatted_msgs = [f"j/{m.strip()}" for m in msgs]
|
|
||||||
|
|
||||||
return message(f"[[BATCH]]{';'.join(formatted_msgs)}", is_json=False)
|
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import sys
|
|
||||||
from typing import Never
|
|
||||||
|
|
||||||
LOG_COLOUR: int = 2
|
|
||||||
INFO_COLOUR: int = 0
|
|
||||||
PROMPT_COLOUR: int = 36
|
|
||||||
WARNING_COLOUR: int = 33
|
|
||||||
ERROR_COLOUR: int = 31
|
|
||||||
|
|
||||||
_disable_input: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def disable_input() -> None:
|
|
||||||
global _disable_input
|
|
||||||
_disable_input = True
|
|
||||||
|
|
||||||
|
|
||||||
def log_exception(func):
|
|
||||||
"""Log exceptions to stderr 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:
|
|
||||||
error(f'exception during "{func.__name__}()": {str(e)}')
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def format_msg(colour: int, prefix: bool, msg: str) -> str:
|
|
||||||
return f"\033[{colour}m{':: ' if prefix else ''}{msg}\033[0m"
|
|
||||||
|
|
||||||
|
|
||||||
def log(msg: str, prefix: bool = True) -> None:
|
|
||||||
print(format_msg(LOG_COLOUR, prefix, msg))
|
|
||||||
|
|
||||||
|
|
||||||
def info(msg: str, prefix: bool = True) -> None:
|
|
||||||
print(format_msg(INFO_COLOUR, prefix, msg))
|
|
||||||
|
|
||||||
|
|
||||||
def warn(msg: str, prefix: bool = True) -> None:
|
|
||||||
print(format_msg(WARNING_COLOUR, prefix, f"Warning: {msg}"))
|
|
||||||
|
|
||||||
|
|
||||||
def error(err: str | Exception, prefix: bool = True) -> None:
|
|
||||||
print(format_msg(ERROR_COLOUR, prefix, f"Error: {err}"), file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def fatal(err: str | Exception, prefix: bool = True) -> Never:
|
|
||||||
print(format_msg(ERROR_COLOUR, prefix, f"Fatal: {err}"), file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _input(prompt: str) -> str:
|
|
||||||
if _disable_input:
|
|
||||||
print(prompt, end="")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
return input(prompt)
|
|
||||||
except (KeyboardInterrupt, EOFError):
|
|
||||||
print()
|
|
||||||
raise KeyboardInterrupt()
|
|
||||||
|
|
||||||
|
|
||||||
def prompt(msg: str, prefix: bool = True, end: str = " ") -> str:
|
|
||||||
return _input(format_msg(PROMPT_COLOUR, prefix, msg) + end)
|
|
||||||
|
|
||||||
|
|
||||||
def confirm(msg: str, prefix: bool = True, default: bool = True) -> bool:
|
|
||||||
suffix = " [Y/n]" if default else " [y/N]"
|
|
||||||
answer = prompt(msg + suffix, prefix=prefix).strip().lower()
|
|
||||||
if not answer:
|
|
||||||
return default
|
|
||||||
return answer in ("y", "yes")
|
|
||||||
|
|
||||||
|
|
||||||
def prompt_selection(items: list[str], header: str) -> list[str]:
|
|
||||||
"""Prompt the user to pick from a numbered list, returning the selected items.
|
|
||||||
|
|
||||||
Accepts `[A]ll`/`a`, single indices, ranges (`1-3`) and exclusions (`^4`).
|
|
||||||
Empty input selects nothing. Re-prompts until the input parses.
|
|
||||||
"""
|
|
||||||
|
|
||||||
print(format_msg(PROMPT_COLOUR, True, header))
|
|
||||||
max_idx_w = len(str(len(items)))
|
|
||||||
for i, item in enumerate(items):
|
|
||||||
print(format_msg(PROMPT_COLOUR, True, f" {i + 1:<{max_idx_w}}\t{item}"))
|
|
||||||
print(format_msg(PROMPT_COLOUR, True, "[A]ll or (1 2 3, 1-3, ^4)"))
|
|
||||||
|
|
||||||
def valid_idx(v: str) -> int:
|
|
||||||
try:
|
|
||||||
idx = int(v, base=10) - 1 # -1 to translate to 0 index
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(f'Given value "{v}" must be an integer.')
|
|
||||||
if idx < 0 or idx >= len(items):
|
|
||||||
raise ValueError(f'Given value "{v}" must be between 1 and {len(items)} inclusive.')
|
|
||||||
return idx
|
|
||||||
|
|
||||||
def parse(ans: str) -> list[str]:
|
|
||||||
if ans in ("a", "all"):
|
|
||||||
return list(items)
|
|
||||||
if not ans:
|
|
||||||
return []
|
|
||||||
|
|
||||||
selected: list[str] = []
|
|
||||||
for tok in ans.split():
|
|
||||||
fr, sep, to = tok.partition("-")
|
|
||||||
if sep:
|
|
||||||
lo, hi = valid_idx(fr), valid_idx(to)
|
|
||||||
if lo > hi:
|
|
||||||
raise ValueError(f'Given range "{tok}" must be lo-hi.')
|
|
||||||
selected += items[lo : hi + 1]
|
|
||||||
elif tok.startswith("^"):
|
|
||||||
t = valid_idx(tok[1:])
|
|
||||||
selected += items[:t] + items[t + 1 :]
|
|
||||||
else:
|
|
||||||
selected.append(items[valid_idx(tok)])
|
|
||||||
return list(set(selected))
|
|
||||||
|
|
||||||
while True:
|
|
||||||
ans = prompt("", end="").lower().strip()
|
|
||||||
try:
|
|
||||||
return parse(ans)
|
|
||||||
except ValueError as e:
|
|
||||||
warn(f"invalid input. {e} Please try again.")
|
|
||||||
|
|
||||||
|
|
||||||
def pause() -> None:
|
|
||||||
if _disable_input:
|
|
||||||
return
|
|
||||||
|
|
||||||
_input("\n\033[2m\033[3m(Ctrl+C to exit, enter to continue)\033[0m")
|
|
||||||
print("\033[1A\r\033[2K\033[1A\r\033[2K", end="") # Clear pause prompt
|
|
||||||
@@ -31,7 +31,7 @@ def get_colours_for_image(image: Path | str = wallpaper_thumbnail_path, scheme=N
|
|||||||
scheme = get_scheme()
|
scheme = get_scheme()
|
||||||
|
|
||||||
cache_base = scheme_cache_dir / compute_hash(image)
|
cache_base = scheme_cache_dir / compute_hash(image)
|
||||||
cache = (cache_base / scheme.variant / scheme.flavour / scheme.mode).with_suffix(".json")
|
cache = (cache_base / scheme.variant / scheme.mode).with_suffix(".json")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with cache.open("r") as f:
|
with cache.open("r") as f:
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from materialyoucolor.blend import Blend
|
from materialyoucolor.blend import Blend
|
||||||
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
|
from materialyoucolor.dynamiccolor.material_dynamic_colors import (
|
||||||
|
DynamicScheme,
|
||||||
|
MaterialDynamicColors,
|
||||||
|
)
|
||||||
from materialyoucolor.hct import Hct
|
from materialyoucolor.hct import Hct
|
||||||
from materialyoucolor.scheme.scheme_content import SchemeContent
|
from materialyoucolor.scheme.scheme_content import SchemeContent
|
||||||
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive
|
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive
|
||||||
@@ -11,19 +14,6 @@ from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow
|
|||||||
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
|
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
|
||||||
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant
|
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant
|
||||||
from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
|
from materialyoucolor.utils.math_utils import difference_degrees, rotation_direction, sanitize_degrees_double
|
||||||
from typing import Protocol, Any
|
|
||||||
|
|
||||||
|
|
||||||
# The base DynamicScheme class requires a 'variant' argument, but the specific
|
|
||||||
# subclasses in get_scheme() handle that internally. This Protocol tells the type
|
|
||||||
# checker to expect our specific 3-argument setup instead of the base class signature.
|
|
||||||
class SchemeConstructor(Protocol):
|
|
||||||
def __call__(self, source_color_hct: Any, is_dark: bool, contrast_level: float) -> "DynamicScheme": ...
|
|
||||||
|
|
||||||
try:
|
|
||||||
from materialyoucolor.dynamiccolor.dynamic_scheme import DynamicScheme
|
|
||||||
except ImportError:
|
|
||||||
from materialyoucolor.scheme.dynamic_scheme import DynamicScheme
|
|
||||||
|
|
||||||
|
|
||||||
def hex_to_hct(hex: str) -> Hct:
|
def hex_to_hct(hex: str) -> Hct:
|
||||||
@@ -102,14 +92,6 @@ 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",
|
||||||
@@ -152,10 +134,10 @@ def lighten(colour: Hct, amount: float) -> Hct:
|
|||||||
|
|
||||||
def darken(colour: Hct, amount: float) -> Hct:
|
def darken(colour: Hct, amount: float) -> Hct:
|
||||||
diff = colour.tone * amount
|
diff = colour.tone * amount
|
||||||
return Hct.from_hct(colour.hue, colour.chroma - diff / 5, colour.tone - diff)
|
return Hct.from_hct(colour.hue, colour.chroma + diff / 5, colour.tone - diff)
|
||||||
|
|
||||||
|
|
||||||
def get_scheme(scheme: str) -> SchemeConstructor:
|
def get_scheme(scheme: str) -> DynamicScheme:
|
||||||
if scheme == "content":
|
if scheme == "content":
|
||||||
return SchemeContent
|
return SchemeContent
|
||||||
if scheme == "expressive":
|
if scheme == "expressive":
|
||||||
@@ -176,62 +158,37 @@ def get_scheme(scheme: str) -> SchemeConstructor:
|
|||||||
|
|
||||||
|
|
||||||
def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
|
def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
|
||||||
is_light = scheme.mode == "light"
|
light = scheme.mode == "light"
|
||||||
|
|
||||||
colours = {}
|
colours = {}
|
||||||
|
|
||||||
# Material colours
|
# Material colours
|
||||||
primary_scheme = get_scheme(scheme.variant)(source_color_hct=primary, is_dark=not is_light, contrast_level=0.0)
|
primary_scheme = get_scheme(scheme.variant)(primary, not light, 0)
|
||||||
if hasattr(MaterialDynamicColors, "all_colors"): # materialyoucolor-python >= 3.0.0
|
for colour in vars(MaterialDynamicColors).keys():
|
||||||
dyn_colours = MaterialDynamicColors()
|
colour_name = getattr(MaterialDynamicColors, colour)
|
||||||
for colour in dyn_colours.all_colors:
|
if hasattr(colour_name, "get_hct"):
|
||||||
colours[colour.name] = colour.get_hct(primary_scheme)
|
colours[colour] = colour_name.get_hct(primary_scheme)
|
||||||
else:
|
|
||||||
for colour in vars(MaterialDynamicColors).keys():
|
|
||||||
colour_name = getattr(MaterialDynamicColors, colour)
|
|
||||||
if hasattr(colour_name, "get_hct"):
|
|
||||||
colours[colour] = colour_name.get_hct(primary_scheme)
|
|
||||||
|
|
||||||
# Backwards compatibility with old colour names
|
|
||||||
if "primaryPaletteKeyColor" in colours: # materialyoucolor-python >= 3.0.0
|
|
||||||
for colour in "primary", "secondary", "tertiary", "neutral":
|
|
||||||
colours[f"{colour}_paletteKeyColor"] = colours[f"{colour}PaletteKeyColor"]
|
|
||||||
colours["neutral_variant_paletteKeyColor"] = colours["neutralVariantPaletteKeyColor"]
|
|
||||||
|
|
||||||
# Harmonize terminal colours
|
# Harmonize terminal colours
|
||||||
for i, hct in enumerate(light_gruvbox if is_light else dark_gruvbox):
|
for i, hct in enumerate(light_gruvbox if light else dark_gruvbox):
|
||||||
if scheme.variant == "monochrome":
|
if scheme.variant == "monochrome":
|
||||||
colours[f"term{i}"] = grayscale(hct, is_light)
|
colours[f"term{i}"] = grayscale(hct, light)
|
||||||
else:
|
else:
|
||||||
colours[f"term{i}"] = harmonize(
|
colours[f"term{i}"] = harmonize(
|
||||||
hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if is_light else 1)
|
hct, colours["primary_paletteKeyColor"], (0.35 if i < 8 else 0.2) * (-1 if light else 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Harmonize named colours
|
# Harmonize named colours
|
||||||
for i, hct in enumerate(light_catppuccin if is_light else dark_catppuccin):
|
for i, hct in enumerate(light_catppuccin if light else dark_catppuccin):
|
||||||
if scheme.variant == "monochrome":
|
if scheme.variant == "monochrome":
|
||||||
colours[colour_names[i]] = grayscale(hct, is_light)
|
colours[colour_names[i]] = grayscale(hct, light)
|
||||||
else:
|
else:
|
||||||
colours[colour_names[i]] = harmonize(hct, colours["primary_paletteKeyColor"], (-0.2 if is_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"]], is_light)
|
|
||||||
colours[f"{colour['name']}Selection"] = grayscale(colours[f"{colour['name']}Selection"], is_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
|
||||||
|
|
||||||
# Darken surfaces for hard flavour
|
|
||||||
if scheme.flavour == "hard":
|
|
||||||
for colour in "background", *(k for k in colours.keys() if k.startswith("surface")):
|
|
||||||
colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.8)
|
|
||||||
colours["term0"] = lighten(colours["term0"], 0.4) if is_light else darken(colours["term0"], 0.9)
|
|
||||||
|
|
||||||
# FIXME: deprecated stuff
|
# FIXME: deprecated stuff
|
||||||
colours["text"] = colours["onBackground"]
|
colours["text"] = colours["onBackground"]
|
||||||
colours["subtext1"] = colours["onSurfaceVariant"]
|
colours["subtext1"] = colours["onSurfaceVariant"]
|
||||||
@@ -246,25 +203,13 @@ def gen_scheme(scheme, primary: Hct) -> dict[str, str]:
|
|||||||
colours["mantle"] = darken(colours["surface"], 0.03)
|
colours["mantle"] = darken(colours["surface"], 0.03)
|
||||||
colours["crust"] = darken(colours["surface"], 0.05)
|
colours["crust"] = darken(colours["surface"], 0.05)
|
||||||
|
|
||||||
# More darkening if hard flavour
|
|
||||||
if scheme.flavour == "hard":
|
|
||||||
for colour in "base", "mantle", "crust":
|
|
||||||
colours[colour] = lighten(colours[colour], 0.4) if is_light else darken(colours[colour], 0.9)
|
|
||||||
for i in range(3):
|
|
||||||
colours[f"overlay{i}"] = (
|
|
||||||
lighten(colours[f"overlay{i}"], 0.4) if is_light else darken(colours[f"overlay{i}"], 0.8)
|
|
||||||
)
|
|
||||||
colours[f"surface{i}"] = (
|
|
||||||
lighten(colours[f"surface{i}"], 0.4) if is_light else darken(colours[f"surface{i}"], 0.8)
|
|
||||||
)
|
|
||||||
|
|
||||||
# For debugging
|
# For debugging
|
||||||
# print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()]))
|
# print("\n".join(["{}: \x1b[48;2;{};{};{}m \x1b[0m".format(n, *c.to_rgba()[:3]) for n, c in colours.items()]))
|
||||||
|
|
||||||
colours = {k: hex(v.to_int())[4:] for k, v in colours.items()}
|
colours = {k: hex(v.to_int())[4:] for k, v in colours.items()}
|
||||||
|
|
||||||
# Extended material
|
# Extended material
|
||||||
if is_light:
|
if light:
|
||||||
colours["success"] = "4F6354"
|
colours["success"] = "4F6354"
|
||||||
colours["onSuccess"] = "FFFFFF"
|
colours["onSuccess"] = "FFFFFF"
|
||||||
colours["successContainer"] = "D1E8D5"
|
colours["successContainer"] = "D1E8D5"
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
def notify(*args: 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,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,50 +1,39 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from caelestia.utils.io import warn
|
config_dir = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
||||||
|
data_dir = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share"))
|
||||||
|
state_dir = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state"))
|
||||||
|
cache_dir = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache"))
|
||||||
|
|
||||||
config_dir: Path = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
c_config_dir = config_dir / "caelestia"
|
||||||
data_dir: Path = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local/share"))
|
c_data_dir = data_dir / "caelestia"
|
||||||
state_dir: Path = Path(os.getenv("XDG_STATE_HOME", Path.home() / ".local/state"))
|
c_state_dir = state_dir / "caelestia"
|
||||||
cache_dir: Path = Path(os.getenv("XDG_CACHE_HOME", Path.home() / ".cache"))
|
c_cache_dir = cache_dir / "caelestia"
|
||||||
pictures_dir: Path = Path(os.getenv("XDG_PICTURES_DIR", Path.home() / "Pictures"))
|
|
||||||
videos_dir: Path = Path(os.getenv("XDG_VIDEOS_DIR", Path.home() / "Videos"))
|
|
||||||
|
|
||||||
c_config_dir: Path = config_dir / "caelestia"
|
cli_data_dir = Path(__file__).parent.parent / "data"
|
||||||
c_data_dir: Path = data_dir / "caelestia"
|
templates_dir = cli_data_dir / "templates"
|
||||||
c_state_dir: Path = state_dir / "caelestia"
|
|
||||||
c_cache_dir: Path = cache_dir / "caelestia"
|
|
||||||
|
|
||||||
user_config_path: Path = c_config_dir / "cli.json"
|
scheme_path = c_state_dir / "scheme.json"
|
||||||
cli_data_dir: Path = Path(__file__).parent.parent / "data"
|
scheme_data_dir = cli_data_dir / "schemes"
|
||||||
templates_dir: Path = cli_data_dir / "templates"
|
scheme_cache_dir = c_cache_dir / "schemes"
|
||||||
user_templates_dir: Path = c_config_dir / "templates"
|
|
||||||
theme_dir: Path = c_state_dir / "theme"
|
|
||||||
|
|
||||||
config_backup_dir: Path = config_dir.parent / f"{config_dir.name}.bak"
|
wallpapers_dir = Path.home() / "Pictures/Wallpapers"
|
||||||
dots_dir: Path = c_state_dir / "dots"
|
wallpaper_path_path = c_state_dir / "wallpaper/path.txt"
|
||||||
dots_state_path: Path = c_state_dir / "dots-state.json"
|
wallpaper_link_path = c_state_dir / "wallpaper/current"
|
||||||
|
wallpaper_thumbnail_path = c_state_dir / "wallpaper/thumbnail.jpg"
|
||||||
|
wallpapers_cache_dir = c_cache_dir / "wallpapers"
|
||||||
|
|
||||||
scheme_path: Path = c_state_dir / "scheme.json"
|
screenshots_dir = Path.home() / "Pictures/Screenshots"
|
||||||
scheme_data_dir: Path = cli_data_dir / "schemes"
|
screenshots_cache_dir = c_cache_dir / "screenshots"
|
||||||
scheme_cache_dir: Path = c_cache_dir / "schemes"
|
|
||||||
|
|
||||||
wallpapers_dir: Path = Path(os.getenv("CAELESTIA_WALLPAPERS_DIR", pictures_dir / "Wallpapers"))
|
recordings_dir = Path.home() / "Videos/Recordings"
|
||||||
wallpaper_path_path: Path = c_state_dir / "wallpaper/path.txt"
|
recording_path = c_state_dir / "record/recording.mp4"
|
||||||
wallpaper_link_path: Path = c_state_dir / "wallpaper/current"
|
recording_notif_path = c_state_dir / "record/notifid.txt"
|
||||||
wallpaper_thumbnail_path: Path = c_state_dir / "wallpaper/thumbnail.jpg"
|
|
||||||
wallpapers_cache_dir: Path = c_cache_dir / "wallpapers"
|
|
||||||
|
|
||||||
screenshots_dir: Path = Path(os.getenv("CAELESTIA_SCREENSHOTS_DIR", pictures_dir / "Screenshots"))
|
|
||||||
screenshots_cache_dir: Path = c_cache_dir / "screenshots"
|
|
||||||
|
|
||||||
recordings_dir: Path = Path(os.getenv("CAELESTIA_RECORDINGS_DIR", videos_dir / "Recordings"))
|
|
||||||
recording_path: Path = c_state_dir / "record/recording.mp4"
|
|
||||||
recording_notif_path: Path = c_state_dir / "record/notifid.txt"
|
|
||||||
|
|
||||||
|
|
||||||
def compute_hash(path: Path | str) -> str:
|
def compute_hash(path: Path | str) -> str:
|
||||||
@@ -57,29 +46,8 @@ def compute_hash(path: Path | str) -> str:
|
|||||||
return sha.hexdigest()
|
return sha.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def atomic_write(path: Path, content: str) -> None:
|
def atomic_dump(path: Path, content: dict[str, any]) -> None:
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
with tempfile.NamedTemporaryFile("w") as f:
|
||||||
f = tempfile.NamedTemporaryFile("w", dir=path.parent, delete=False)
|
json.dump(content, f)
|
||||||
try:
|
f.flush()
|
||||||
with f:
|
shutil.move(f.name, path)
|
||||||
f.write(content)
|
|
||||||
f.flush()
|
|
||||||
os.fsync(f.fileno())
|
|
||||||
os.replace(f.name, path)
|
|
||||||
except BaseException:
|
|
||||||
os.unlink(f.name)
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def atomic_dump(path: Path, content: dict[str, Any]) -> None:
|
|
||||||
atomic_write(path, json.dumps(content))
|
|
||||||
|
|
||||||
|
|
||||||
def get_config() -> dict[str, Any]:
|
|
||||||
try:
|
|
||||||
return json.loads(user_config_path.read_text())
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
warn("failed to parse config, invalid JSON")
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
return {}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from caelestia.utils.notify import notify
|
from caelestia.utils.notify import notify
|
||||||
from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path
|
from caelestia.utils.paths import atomic_dump, scheme_data_dir, scheme_path
|
||||||
@@ -15,19 +14,19 @@ class Scheme:
|
|||||||
_colours: dict[str, str]
|
_colours: dict[str, str]
|
||||||
notify: bool
|
notify: bool
|
||||||
|
|
||||||
def __init__(self, scheme_json: dict[str, Any] | None) -> None:
|
def __init__(self, json: dict[str, any] | None) -> None:
|
||||||
if scheme_json is None:
|
if json is None:
|
||||||
self._name = "catppuccin"
|
self._name = "catppuccin"
|
||||||
self._flavour = "mocha"
|
self._flavour = "mocha"
|
||||||
self._mode = "dark"
|
self._mode = "dark"
|
||||||
self._variant = "tonalspot"
|
self._variant = "tonalspot"
|
||||||
self._colours = read_colours_from_file(self.get_colours_path())
|
self._colours = read_colours_from_file(self.get_colours_path())
|
||||||
else:
|
else:
|
||||||
self._name = scheme_json["name"]
|
self._name = json["name"]
|
||||||
self._flavour = scheme_json["flavour"]
|
self._flavour = json["flavour"]
|
||||||
self._mode = scheme_json["mode"]
|
self._mode = json["mode"]
|
||||||
self._variant = scheme_json["variant"]
|
self._variant = json["variant"]
|
||||||
self._colours = scheme_json["colours"]
|
self._colours = json["colours"]
|
||||||
self.notify = False
|
self.notify = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -197,11 +196,11 @@ scheme_variants = [
|
|||||||
"content",
|
"content",
|
||||||
]
|
]
|
||||||
|
|
||||||
scheme: Scheme | None = None
|
scheme: Scheme = None
|
||||||
|
|
||||||
|
|
||||||
def read_colours_from_file(path: Path) -> dict[str, str]:
|
def read_colours_from_file(path: Path) -> dict[str, str]:
|
||||||
return {k.strip(): v.strip() for k, v in (line.split(" ") for line in path.read_text().splitlines() if line)}
|
return {k.strip(): v.strip() for k, v in (line.split(" ") for line in path.read_text().splitlines())}
|
||||||
|
|
||||||
|
|
||||||
def get_scheme_path() -> Path:
|
def get_scheme_path() -> Path:
|
||||||
@@ -226,20 +225,18 @@ def get_scheme_names() -> list[str]:
|
|||||||
return [*(f.name for f in scheme_data_dir.iterdir() if f.is_dir()), "dynamic"]
|
return [*(f.name for f in scheme_data_dir.iterdir() if f.is_dir()), "dynamic"]
|
||||||
|
|
||||||
|
|
||||||
def get_scheme_flavours(name: str | None = None) -> list[str]:
|
def get_scheme_flavours(name: str = None) -> list[str]:
|
||||||
if name is None:
|
if name is None:
|
||||||
name = get_scheme().name
|
name = get_scheme().name
|
||||||
|
|
||||||
return (
|
return ["default"] if name == "dynamic" else [f.name for f in (scheme_data_dir / name).iterdir() if f.is_dir()]
|
||||||
["default", "hard"] if name == "dynamic" else [f.name for f in (scheme_data_dir / name).iterdir() if f.is_dir()]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_scheme_modes(name: str | None = None, flavour: str | None = None) -> list[str]:
|
def get_scheme_modes(name: str = None, flavour: str = None) -> list[str]:
|
||||||
if name is None or flavour is None:
|
if name is None:
|
||||||
scheme = get_scheme()
|
scheme = get_scheme()
|
||||||
name = name or scheme.name
|
name = scheme.name
|
||||||
flavour = flavour or scheme.flavour
|
flavour = scheme.flavour
|
||||||
|
|
||||||
if name == "dynamic":
|
if name == "dynamic":
|
||||||
return ["light", "dark"]
|
return ["light", "dark"]
|
||||||
|
|||||||
+64
-392
@@ -1,26 +1,7 @@
|
|||||||
import fcntl
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from caelestia.utils.colour import get_dynamic_colours
|
from caelestia.utils.paths import c_state_dir, config_dir, templates_dir
|
||||||
from caelestia.utils.hypr import is_lua_config
|
|
||||||
from caelestia.utils.io import log_exception
|
|
||||||
from caelestia.utils.paths import (
|
|
||||||
atomic_write,
|
|
||||||
c_state_dir,
|
|
||||||
config_dir,
|
|
||||||
data_dir,
|
|
||||||
get_config,
|
|
||||||
templates_dir,
|
|
||||||
theme_dir,
|
|
||||||
user_templates_dir,
|
|
||||||
)
|
|
||||||
from caelestia.utils.scheme import get_scheme
|
|
||||||
|
|
||||||
|
|
||||||
def gen_conf(colours: dict[str, str]) -> str:
|
def gen_conf(colours: dict[str, str]) -> str:
|
||||||
@@ -30,14 +11,6 @@ def gen_conf(colours: dict[str, str]) -> str:
|
|||||||
return conf
|
return conf
|
||||||
|
|
||||||
|
|
||||||
def gen_lua(colours: dict[str, str]) -> str:
|
|
||||||
lua = "return {\n"
|
|
||||||
for name, colour in colours.items():
|
|
||||||
lua += f' {name} = "{colour}",\n'
|
|
||||||
lua += "}"
|
|
||||||
return lua
|
|
||||||
|
|
||||||
|
|
||||||
def gen_scss(colours: dict[str, str]) -> str:
|
def gen_scss(colours: dict[str, str]) -> str:
|
||||||
scss = ""
|
scss = ""
|
||||||
for name, colour in colours.items():
|
for name, colour in colours.items():
|
||||||
@@ -46,38 +19,13 @@ def gen_scss(colours: dict[str, str]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) -> str:
|
def gen_replace(colours: dict[str, str], template: Path, hash: bool = False) -> str:
|
||||||
new_template = template.read_text()
|
template = template.read_text()
|
||||||
for name, colour in colours.items():
|
for name, colour in colours.items():
|
||||||
new_template = new_template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour)
|
template = template.replace(f"{{{{ ${name} }}}}", f"#{colour}" if hash else colour)
|
||||||
return new_template
|
return template
|
||||||
|
|
||||||
|
|
||||||
def gen_replace_dynamic(colours: dict[str, str], template: Path, mode: str) -> str:
|
def c2s(c: str, *i: list[int]) -> str:
|
||||||
def fill_colour(match: re.Match) -> str:
|
|
||||||
data = match.group(1).strip().split(".")
|
|
||||||
if len(data) != 2:
|
|
||||||
return match.group()
|
|
||||||
col, form = data
|
|
||||||
if col not in colours_dyn or not hasattr(colours_dyn[col], form):
|
|
||||||
return match.group()
|
|
||||||
return getattr(colours_dyn[col], form)
|
|
||||||
|
|
||||||
# match atomic {{ . }} pairs
|
|
||||||
dotField = r"\{\{((?:(?!\{\{|\}\}).)*)\}\}"
|
|
||||||
|
|
||||||
# match {{ mode }}
|
|
||||||
modeField = r"\{\{\s*mode\s*\}\}"
|
|
||||||
|
|
||||||
colours_dyn = get_dynamic_colours(colours)
|
|
||||||
template_content = template.read_text()
|
|
||||||
|
|
||||||
template_filled = re.sub(dotField, fill_colour, template_content)
|
|
||||||
template_filled = re.sub(modeField, mode, template_filled)
|
|
||||||
|
|
||||||
return template_filled
|
|
||||||
|
|
||||||
|
|
||||||
def hex_to_ansi(c: str, *i: int) -> str:
|
|
||||||
"""Hex to ANSI sequence (e.g. ffffff, 11 -> \x1b]11;rgb:ff/ff/ff\x1b\\)"""
|
"""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\\"
|
||||||
|
|
||||||
@@ -94,33 +42,37 @@ def gen_sequences(colours: dict[str, str]) -> str:
|
|||||||
16+: 256 colours
|
16+: 256 colours
|
||||||
"""
|
"""
|
||||||
return (
|
return (
|
||||||
hex_to_ansi(colours["onSurface"], 10)
|
c2s(colours["onSurface"], 10)
|
||||||
+ hex_to_ansi(colours["surface"], 11)
|
+ c2s(colours["surface"], 11)
|
||||||
+ hex_to_ansi(colours["secondary"], 12)
|
+ c2s(colours["secondary"], 12)
|
||||||
+ hex_to_ansi(colours["secondary"], 17)
|
+ c2s(colours["secondary"], 17)
|
||||||
+ hex_to_ansi(colours["term0"], 4, 0)
|
+ c2s(colours["term0"], 4, 0)
|
||||||
+ hex_to_ansi(colours["term1"], 4, 1)
|
+ c2s(colours["term1"], 4, 1)
|
||||||
+ hex_to_ansi(colours["term2"], 4, 2)
|
+ c2s(colours["term2"], 4, 2)
|
||||||
+ hex_to_ansi(colours["term3"], 4, 3)
|
+ c2s(colours["term3"], 4, 3)
|
||||||
+ hex_to_ansi(colours["term4"], 4, 4)
|
+ c2s(colours["term4"], 4, 4)
|
||||||
+ hex_to_ansi(colours["term5"], 4, 5)
|
+ c2s(colours["term5"], 4, 5)
|
||||||
+ hex_to_ansi(colours["term6"], 4, 6)
|
+ c2s(colours["term6"], 4, 6)
|
||||||
+ hex_to_ansi(colours["term7"], 4, 7)
|
+ c2s(colours["term7"], 4, 7)
|
||||||
+ hex_to_ansi(colours["term8"], 4, 8)
|
+ c2s(colours["term8"], 4, 8)
|
||||||
+ hex_to_ansi(colours["term9"], 4, 9)
|
+ c2s(colours["term9"], 4, 9)
|
||||||
+ hex_to_ansi(colours["term10"], 4, 10)
|
+ c2s(colours["term10"], 4, 10)
|
||||||
+ hex_to_ansi(colours["term11"], 4, 11)
|
+ c2s(colours["term11"], 4, 11)
|
||||||
+ hex_to_ansi(colours["term12"], 4, 12)
|
+ c2s(colours["term12"], 4, 12)
|
||||||
+ hex_to_ansi(colours["term13"], 4, 13)
|
+ c2s(colours["term13"], 4, 13)
|
||||||
+ hex_to_ansi(colours["term14"], 4, 14)
|
+ c2s(colours["term14"], 4, 14)
|
||||||
+ hex_to_ansi(colours["term15"], 4, 15)
|
+ c2s(colours["term15"], 4, 15)
|
||||||
+ hex_to_ansi(colours["primary"], 4, 16)
|
+ c2s(colours["primary"], 4, 16)
|
||||||
+ hex_to_ansi(colours["secondary"], 4, 17)
|
+ c2s(colours["secondary"], 4, 17)
|
||||||
+ hex_to_ansi(colours["tertiary"], 4, 18)
|
+ c2s(colours["tertiary"], 4, 18)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
def write_file(path: Path, content: str) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(content)
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -129,350 +81,70 @@ 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():
|
||||||
try:
|
with pt.open("a") as f:
|
||||||
# Use non-blocking write with timeout to prevent hangs
|
f.write(sequences)
|
||||||
import os
|
|
||||||
|
|
||||||
fd = os.open(str(pt), os.O_WRONLY | os.O_NONBLOCK | os.O_NOCTTY)
|
|
||||||
try:
|
|
||||||
os.write(fd, sequences.encode())
|
|
||||||
finally:
|
|
||||||
os.close(fd)
|
|
||||||
except (PermissionError, OSError, BlockingIOError):
|
|
||||||
# Skip terminals that are busy, closed, or inaccessible
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_hypr(conf: str) -> None:
|
def apply_hypr(conf: str) -> None:
|
||||||
ext = "lua" if is_lua_config() else "conf"
|
write_file(config_dir / "hypr/scheme/current.conf", conf)
|
||||||
atomic_write(config_dir / f"hypr/scheme/current.{ext}", conf)
|
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_discord(scss: str) -> None:
|
def apply_discord(scss: str) -> None:
|
||||||
|
import tempfile
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory("w") as tmp_dir:
|
with tempfile.TemporaryDirectory("w") as tmp_dir:
|
||||||
(Path(tmp_dir) / "_colours.scss").write_text(scss)
|
(Path(tmp_dir) / "_colours.scss").write_text(scss)
|
||||||
conf = subprocess.check_output(["sass", "-I", tmp_dir, templates_dir / "discord.scss"], text=True)
|
conf = subprocess.check_output(["sass", "-I", tmp_dir, templates_dir / "discord.scss"], text=True)
|
||||||
|
|
||||||
for client in "Equicord", "Vencord", "BetterDiscord", "equibop", "vesktop", "legcord":
|
for client in "Equicord", "Vencord", "BetterDiscord", "equibop", "vesktop", "legcord":
|
||||||
atomic_write(config_dir / client / "themes/caelestia.theme.css", conf)
|
write_file(config_dir / client / "themes/caelestia.theme.css", conf)
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_pandora(colours: dict[str, str], mode: str) -> None:
|
|
||||||
template = gen_replace(colours, templates_dir / "pandora.json", hash=True)
|
|
||||||
template = template.replace("{{ $mode }}", mode)
|
|
||||||
atomic_write(data_dir / "PandoraLauncher/themes/caelestia.json", template)
|
|
||||||
|
|
||||||
|
|
||||||
@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")
|
||||||
atomic_write(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")
|
||||||
atomic_write(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)
|
||||||
atomic_write(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_gtk(colours: dict[str, str], mode: str) -> None:
|
||||||
def apply_nvtop(colours: dict[str, str]) -> None:
|
template = gen_replace(colours, templates_dir / "gtk.css", hash=True)
|
||||||
template = gen_replace(colours, templates_dir / "nvtop.colors", hash=True)
|
write_file(config_dir / "gtk-3.0/gtk.css", template)
|
||||||
atomic_write(config_dir / "nvtop/nvtop.colors", template)
|
write_file(config_dir / "gtk-4.0/gtk.css", template)
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_htop(colours: dict[str, str]) -> None:
|
|
||||||
template = gen_replace(colours, templates_dir / "htop.theme", hash=True)
|
|
||||||
atomic_write(config_dir / "htop/htoprc", template)
|
|
||||||
subprocess.run(["killall", "-USR2", "htop"], stderr=subprocess.DEVNULL)
|
|
||||||
|
|
||||||
|
|
||||||
def sync_papirus_colors(hex_color: str) -> None:
|
|
||||||
"""Sync Papirus folder icon colors using hue/saturation analysis"""
|
|
||||||
try:
|
|
||||||
result = subprocess.run(["which", "papirus-folders"], capture_output=True, check=False)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
papirus_paths = [
|
|
||||||
Path("/usr/share/icons/Papirus"),
|
|
||||||
Path("/usr/share/icons/Papirus-Dark"),
|
|
||||||
Path.home() / ".local/share/icons/Papirus",
|
|
||||||
Path.home() / ".icons/Papirus",
|
|
||||||
]
|
|
||||||
|
|
||||||
if not any(p.exists() for p in papirus_paths):
|
|
||||||
return
|
|
||||||
|
|
||||||
r = int(hex_color[0:2], 16)
|
|
||||||
g = int(hex_color[2:4], 16)
|
|
||||||
b = int(hex_color[4:6], 16)
|
|
||||||
|
|
||||||
# Brightness and saturation
|
|
||||||
max_val = max(r, g, b)
|
|
||||||
min_val = min(r, g, b)
|
|
||||||
brightness = max_val
|
|
||||||
saturation = 0 if max_val == 0 else ((max_val - min_val) * 100) // max_val
|
|
||||||
|
|
||||||
# Low saturation = grayscale
|
|
||||||
if saturation < 20:
|
|
||||||
if brightness < 85:
|
|
||||||
color = "black"
|
|
||||||
elif brightness < 170:
|
|
||||||
color = "grey"
|
|
||||||
else:
|
|
||||||
color = "white"
|
|
||||||
# Medium-low saturation with high brightness = pale variants
|
|
||||||
elif saturation < 60 and brightness > 180:
|
|
||||||
use_pale = True
|
|
||||||
color = _determine_hue_color(r, g, b, brightness, use_pale)
|
|
||||||
else:
|
|
||||||
color = _determine_hue_color(r, g, b, brightness, False)
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.Popen(
|
|
||||||
["sudo", "-n", "papirus-folders", "-C", color, "-u"],
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
start_new_session=True,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _determine_hue_color(r: int, g: int, b: int, brightness: int, use_pale: bool) -> str:
|
|
||||||
if b > r and b > g:
|
|
||||||
# Blue dominant
|
|
||||||
r_ratio = (r * 100) // b if b > 0 else 0
|
|
||||||
g_ratio = (g * 100) // b if b > 0 else 0
|
|
||||||
rg_diff = abs(r - g)
|
|
||||||
|
|
||||||
if r_ratio > 70 and g_ratio > 70:
|
|
||||||
# Both R and G high relative to B = light blue/periwinkle
|
|
||||||
if rg_diff < 15:
|
|
||||||
return "blue"
|
|
||||||
elif r > g:
|
|
||||||
return "violet"
|
|
||||||
else:
|
|
||||||
return "cyan"
|
|
||||||
elif r_ratio > 60 and r > g:
|
|
||||||
return "violet"
|
|
||||||
elif g_ratio > 60 and g > r:
|
|
||||||
return "cyan"
|
|
||||||
else:
|
|
||||||
return "blue"
|
|
||||||
elif r > g and r > b:
|
|
||||||
# Red dominant
|
|
||||||
if g > b + 30:
|
|
||||||
# Orange/yellow-ish/brown
|
|
||||||
rg_ratio = (g * 100) // r if r > 0 else 0
|
|
||||||
if use_pale:
|
|
||||||
if rg_ratio > 70 and brightness < 220:
|
|
||||||
return "palebrown"
|
|
||||||
else:
|
|
||||||
return "paleorange"
|
|
||||||
else:
|
|
||||||
if rg_ratio > 70 and brightness < 180:
|
|
||||||
return "brown"
|
|
||||||
else:
|
|
||||||
return "orange"
|
|
||||||
elif b > g + 20:
|
|
||||||
return "pink"
|
|
||||||
else:
|
|
||||||
return "pink" if use_pale else "red"
|
|
||||||
elif g > r and g > b:
|
|
||||||
# Green dominant
|
|
||||||
if r > b + 30:
|
|
||||||
return "yellow"
|
|
||||||
else:
|
|
||||||
return "green"
|
|
||||||
else:
|
|
||||||
return "grey"
|
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_gtk(colours: dict[str, str], mode: str, icon_theme: str | None = None) -> None:
|
|
||||||
gtk_template = gen_replace(colours, templates_dir / "gtk.css", hash=True)
|
|
||||||
thunar_template = gen_replace(colours, templates_dir / "thunar.css", hash=True)
|
|
||||||
|
|
||||||
for gtk_version in ["gtk-3.0", "gtk-4.0"]:
|
|
||||||
gtk_config_dir = config_dir / gtk_version
|
|
||||||
atomic_write(gtk_config_dir / "gtk.css", gtk_template)
|
|
||||||
atomic_write(gtk_config_dir / "thunar.css", thunar_template)
|
|
||||||
|
|
||||||
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/gtk-theme", "'adw-gtk3-dark'"])
|
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/gtk-theme", "'adw-gtk3-dark'"])
|
||||||
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'prefer-{mode}'"])
|
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'prefer-{mode}'"])
|
||||||
gtk_icon_theme = icon_theme if icon_theme is not None else f"Papirus-{mode.capitalize()}"
|
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'Papirus-{mode.capitalize()}'"])
|
||||||
subprocess.run(["dconf", "write", "/org/gnome/desktop/interface/icon-theme", f"'{gtk_icon_theme}'"])
|
|
||||||
|
|
||||||
sync_papirus_colors(colours["primary"])
|
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
def apply_qt(colours: dict[str, str], mode: str) -> None:
|
||||||
def apply_qt(colours: dict[str, str], mode: str, icon_theme: str | None = None) -> None:
|
template = gen_replace(colours, templates_dir / "qtcolors.conf", hash=True)
|
||||||
colours = gen_replace(colours, templates_dir / f"qt{mode}.colors", hash=True)
|
write_file(config_dir / "qt5ct/colors/caelestia.conf", template)
|
||||||
atomic_write(config_dir / "qtengine/caelestia.colors", colours)
|
write_file(config_dir / "qt6ct/colors/caelestia.conf", template)
|
||||||
|
|
||||||
config = (templates_dir / "qtengine.json").read_text()
|
qtct = (templates_dir / "qtct.conf").read_text()
|
||||||
config = config.replace("{{ $mode }}", mode.capitalize())
|
qtct = qtct.replace("{{ $mode }}", mode.capitalize())
|
||||||
if icon_theme is not None:
|
|
||||||
config = config.replace(f'"iconTheme": "Papirus-{mode.capitalize()}"', f'"iconTheme": "{icon_theme}"')
|
|
||||||
atomic_write(config_dir / "qtengine/config.json", config)
|
|
||||||
|
|
||||||
|
for ver in 5, 6:
|
||||||
@log_exception
|
conf = qtct.replace("{{ $config }}", str(config_dir / f"qt{ver}ct"))
|
||||||
def apply_warp(colours: dict[str, str], mode: str) -> None:
|
write_file(config_dir / f"qt{ver}ct/qt{ver}ct.conf", conf)
|
||||||
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)
|
|
||||||
atomic_write(data_dir / "warp-terminal/themes/caelestia.yaml", template)
|
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_chromium(colours: dict[str, str]) -> None:
|
|
||||||
surface_hex = colours["surface"]
|
|
||||||
theme_color = f"#{surface_hex}"
|
|
||||||
browsers = [
|
|
||||||
("chromium", Path("/etc/chromium/policies/managed")),
|
|
||||||
("brave", Path("/etc/brave/policies/managed")),
|
|
||||||
("google-chrome-stable", Path("/etc/opt/chrome/policies/managed")),
|
|
||||||
]
|
|
||||||
|
|
||||||
for cmd, policy_dir in browsers:
|
|
||||||
if shutil.which(cmd) is None:
|
|
||||||
continue
|
|
||||||
if not policy_dir.is_dir():
|
|
||||||
subprocess.run(["sudo", "-n", "mkdir", "-p", str(policy_dir)], stderr=subprocess.DEVNULL)
|
|
||||||
if not policy_dir.is_dir():
|
|
||||||
print(f"Unable to create {policy_dir} directory")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Use tee instead of atomic_write cause we need sudo
|
|
||||||
subprocess.run(
|
|
||||||
["sudo", "-n", "tee", str(policy_dir / "caelestia.json")],
|
|
||||||
input=json.dumps({"BrowserThemeColor": theme_color, "BrowserColorScheme": "device"}),
|
|
||||||
text=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
subprocess.run(
|
|
||||||
[cmd, "--refresh-platform-policy", "--no-startup-window"],
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_zed(colours: dict[str, str], mode: str) -> None:
|
|
||||||
theme_path = config_dir / "zed/themes/caelestia.json"
|
|
||||||
# Zed's file watcher does not detect changes through symlinks,
|
|
||||||
# so resolve to a regular file before writing
|
|
||||||
if theme_path.is_symlink():
|
|
||||||
theme_path.unlink()
|
|
||||||
|
|
||||||
content = gen_replace_dynamic(colours, templates_dir / "zed.json", mode)
|
|
||||||
atomic_write(theme_path, content)
|
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_cava(colours: dict[str, str]) -> None:
|
|
||||||
template = gen_replace(colours, templates_dir / "cava.conf", hash=True)
|
|
||||||
atomic_write(config_dir / "cava/config", template)
|
|
||||||
subprocess.run(["killall", "-USR2", "cava"], stderr=subprocess.DEVNULL)
|
|
||||||
|
|
||||||
|
|
||||||
@log_exception
|
|
||||||
def apply_user_templates(colours: dict[str, str], mode: str) -> None:
|
|
||||||
if not user_templates_dir.is_dir():
|
|
||||||
return
|
|
||||||
|
|
||||||
for file in user_templates_dir.iterdir():
|
|
||||||
if file.is_file():
|
|
||||||
content = gen_replace_dynamic(colours, file, mode)
|
|
||||||
atomic_write(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:
|
||||||
# Use file-based lock to prevent concurrent theme changes
|
apply_terms(gen_sequences(colours))
|
||||||
lock_file = c_state_dir / "theme.lock"
|
apply_hypr(gen_conf(colours))
|
||||||
c_state_dir.mkdir(parents=True, exist_ok=True)
|
apply_discord(gen_scss(colours))
|
||||||
|
apply_spicetify(colours, mode)
|
||||||
try:
|
apply_fuzzel(colours)
|
||||||
with open(lock_file, "w") as lock_fd:
|
apply_btop(colours)
|
||||||
try:
|
apply_gtk(colours, mode)
|
||||||
fcntl.flock(lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
apply_qt(colours, mode)
|
||||||
except BlockingIOError:
|
|
||||||
return
|
|
||||||
|
|
||||||
cfg = get_config().get("theme", {})
|
|
||||||
|
|
||||||
def check(key: str) -> bool:
|
|
||||||
return cfg[key] if key in cfg else True
|
|
||||||
|
|
||||||
if check("enableTerm"):
|
|
||||||
apply_terms(gen_sequences(colours))
|
|
||||||
if check("enableHypr"):
|
|
||||||
apply_hypr(gen_lua(colours) if is_lua_config() else gen_conf(colours))
|
|
||||||
if check("enableDiscord"):
|
|
||||||
apply_discord(gen_scss(colours))
|
|
||||||
if check("enableSpicetify"):
|
|
||||||
apply_spicetify(colours, mode)
|
|
||||||
if check("enablePandora"):
|
|
||||||
apply_pandora(colours, mode)
|
|
||||||
if check("enableFuzzel"):
|
|
||||||
apply_fuzzel(colours)
|
|
||||||
if check("enableBtop"):
|
|
||||||
apply_btop(colours)
|
|
||||||
if check("enableNvtop"):
|
|
||||||
apply_nvtop(colours)
|
|
||||||
if check("enableHtop"):
|
|
||||||
apply_htop(colours)
|
|
||||||
icon_theme = cfg.get(f"iconTheme{mode.capitalize()}") or cfg.get("iconTheme")
|
|
||||||
if check("enableGtk"):
|
|
||||||
apply_gtk(colours, mode, icon_theme)
|
|
||||||
if check("enableQt"):
|
|
||||||
apply_qt(colours, mode, icon_theme)
|
|
||||||
if check("enableWarp"):
|
|
||||||
apply_warp(colours, mode)
|
|
||||||
if check("enableChromium"):
|
|
||||||
apply_chromium(colours)
|
|
||||||
if check("enableZed"):
|
|
||||||
apply_zed(colours, mode)
|
|
||||||
if check("enableCava"):
|
|
||||||
apply_cava(colours)
|
|
||||||
apply_user_templates(colours, mode)
|
|
||||||
|
|
||||||
if post_hook := cfg.get("postHook"):
|
|
||||||
scheme = get_scheme()
|
|
||||||
subprocess.run(
|
|
||||||
post_hook,
|
|
||||||
shell=True,
|
|
||||||
env={
|
|
||||||
**os.environ,
|
|
||||||
"SCHEME_NAME": scheme.name,
|
|
||||||
"SCHEME_FLAVOUR": scheme.flavour,
|
|
||||||
"SCHEME_MODE": scheme.mode,
|
|
||||||
"SCHEME_VARIANT": scheme.variant,
|
|
||||||
"SCHEME_COLOURS": json.dumps(scheme.colours),
|
|
||||||
},
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
lock_file.unlink()
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,50 +1,30 @@
|
|||||||
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:
|
||||||
if shutil.which("pacman"):
|
print("Packages:")
|
||||||
print("Packages:")
|
pkgs = ["caelestia-shell-git", "caelestia-cli-git", "caelestia-meta"]
|
||||||
pkgs = ["caelestia-shell", "caelestia-cli", "caelestia-meta"]
|
versions = subprocess.run(
|
||||||
versions = subprocess.run(
|
["pacman", "-Q", *pkgs], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
|
||||||
["pacman", "-Q", *pkgs], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True
|
).stdout
|
||||||
).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")
|
|
||||||
|
|
||||||
print()
|
caelestia_dir = (config_dir / "hypr").resolve().parent
|
||||||
try:
|
print("\nCaelestia:")
|
||||||
caelestia_dir = (config_dir / "hypr").resolve().parent
|
caelestia_ver = subprocess.check_output(
|
||||||
caelestia_ver = subprocess.check_output(
|
["git", "--git-dir", caelestia_dir / ".git", "rev-list", "--format=%B", "--max-count=1", "HEAD"], text=True
|
||||||
["git", "--git-dir", caelestia_dir / ".git", "rev-list", "--format=%B", "--max-count=1", "HEAD"], text=True
|
)
|
||||||
)
|
print(" Last commit:", caelestia_ver.split()[1])
|
||||||
print("Caelestia:")
|
print(" Commit message:", *caelestia_ver.splitlines()[1:])
|
||||||
print(" Last commit:", caelestia_ver.split()[1])
|
|
||||||
print(" Commit message:", *caelestia_ver.splitlines()[1:])
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print("Caelestia: not installed")
|
|
||||||
|
|
||||||
print()
|
print("\nQuickshell:")
|
||||||
try:
|
print(" ", subprocess.check_output(["qs", "--version"], text=True).strip())
|
||||||
shell_ver = subprocess.check_output(["/usr/lib/caelestia/version", "-s"], text=True).strip()
|
|
||||||
print("Shell:")
|
|
||||||
print(" ", shell_ver)
|
|
||||||
except FileNotFoundError:
|
|
||||||
print("Shell: version helper not available")
|
|
||||||
|
|
||||||
print()
|
|
||||||
if shutil.which("qs"):
|
|
||||||
print("Quickshell:")
|
|
||||||
print(" ", subprocess.check_output(["qs", "--version"], text=True).strip())
|
|
||||||
else:
|
|
||||||
print("Quickshell: not in PATH")
|
|
||||||
|
|
||||||
local_shell_dir = config_dir / "quickshell/caelestia"
|
local_shell_dir = config_dir / "quickshell/caelestia"
|
||||||
if local_shell_dir.exists():
|
if local_shell_dir.exists():
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import subprocess
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from materialyoucolor.hct import Hct
|
from materialyoucolor.hct import Hct
|
||||||
from materialyoucolor.utils.color_utils import argb_from_rgb
|
from materialyoucolor.utils.color_utils import argb_from_rgb
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from caelestia.utils.colourfulness import get_variant
|
|
||||||
from caelestia.utils.hypr import message
|
from caelestia.utils.hypr import message
|
||||||
from caelestia.utils.material import get_colours_for_image
|
from caelestia.utils.material import get_colours_for_image
|
||||||
from caelestia.utils.paths import (
|
from caelestia.utils.paths import (
|
||||||
compute_hash,
|
compute_hash,
|
||||||
get_config,
|
|
||||||
wallpaper_link_path,
|
wallpaper_link_path,
|
||||||
wallpaper_path_path,
|
wallpaper_path_path,
|
||||||
wallpaper_thumbnail_path,
|
wallpaper_thumbnail_path,
|
||||||
@@ -26,7 +21,7 @@ from caelestia.utils.theme import apply_colours
|
|||||||
|
|
||||||
|
|
||||||
def is_valid_image(path: Path) -> bool:
|
def is_valid_image(path: Path) -> bool:
|
||||||
return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff", ".gif"]
|
return path.is_file() and path.suffix in [".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff"]
|
||||||
|
|
||||||
|
|
||||||
def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bool:
|
def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bool:
|
||||||
@@ -35,7 +30,7 @@ def check_wall(wall: Path, filter_size: tuple[int, int], threshold: float) -> bo
|
|||||||
return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold
|
return width >= filter_size[0] * threshold and height >= filter_size[1] * threshold
|
||||||
|
|
||||||
|
|
||||||
def get_wallpaper() -> str | None:
|
def get_wallpaper() -> str:
|
||||||
try:
|
try:
|
||||||
return wallpaper_path_path.read_text()
|
return wallpaper_path_path.read_text()
|
||||||
except IOError:
|
except IOError:
|
||||||
@@ -43,17 +38,22 @@ def get_wallpaper() -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def get_wallpapers(args: Namespace) -> list[Path]:
|
def get_wallpapers(args: Namespace) -> list[Path]:
|
||||||
directory = Path(args.random)
|
dir = Path(args.random)
|
||||||
if not directory.is_dir():
|
if not dir.is_dir():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
walls = [f for f in directory.rglob("*") if is_valid_image(f)]
|
walls = [f for f in dir.rglob("*") if is_valid_image(f)]
|
||||||
|
|
||||||
if args.no_filter:
|
if args.no_filter:
|
||||||
return walls
|
return walls
|
||||||
|
|
||||||
monitors = cast(list[dict[str, int]], message("monitors"))
|
monitors = message("monitors")
|
||||||
filter_size = min(m["width"] for m in monitors), min(m["height"] for m in monitors)
|
filter_size = monitors[0]["width"], monitors[0]["height"]
|
||||||
|
for monitor in monitors[1:]:
|
||||||
|
if filter_size[0] > monitor["width"]:
|
||||||
|
filter_size[0] = monitor["width"]
|
||||||
|
if filter_size[1] > monitor["height"]:
|
||||||
|
filter_size[1] = monitor["height"]
|
||||||
|
|
||||||
return [f for f in walls if check_wall(f, filter_size, args.threshold)]
|
return [f for f in walls if check_wall(f, filter_size, args.threshold)]
|
||||||
|
|
||||||
@@ -64,14 +64,14 @@ def get_thumb(wall: Path, cache: Path) -> Path:
|
|||||||
if not thumb.exists():
|
if not thumb.exists():
|
||||||
with Image.open(wall) as img:
|
with Image.open(wall) as img:
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
img.thumbnail((128, 128), Image.Resampling.NEAREST)
|
img.thumbnail((128, 128), Image.NEAREST)
|
||||||
thumb.parent.mkdir(parents=True, exist_ok=True)
|
thumb.parent.mkdir(parents=True, exist_ok=True)
|
||||||
img.save(thumb, "JPEG")
|
img.save(thumb, "JPEG")
|
||||||
|
|
||||||
return thumb
|
return thumb
|
||||||
|
|
||||||
|
|
||||||
def get_smart_opts(wall: Path, cache: Path) -> dict:
|
def get_smart_opts(wall: Path, cache: Path) -> str:
|
||||||
opts_cache = cache / "smart.json"
|
opts_cache = cache / "smart.json"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -79,16 +79,15 @@ def get_smart_opts(wall: Path, cache: Path) -> dict:
|
|||||||
except (IOError, json.JSONDecodeError):
|
except (IOError, json.JSONDecodeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
from caelestia.utils.colourfulness import get_variant
|
||||||
|
|
||||||
opts = {}
|
opts = {}
|
||||||
|
|
||||||
with Image.open(get_thumb(wall, cache)) as img:
|
with Image.open(get_thumb(wall, cache)) as img:
|
||||||
opts["variant"] = get_variant(img)
|
opts["variant"] = get_variant(img)
|
||||||
img.thumbnail((1, 1), Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
# Cast the pixel to a tuple of 3 integers to safely unpack it
|
|
||||||
pixel = cast(tuple[int, int, int], img.getpixel((0, 0)))
|
|
||||||
hct = Hct.from_int(argb_from_rgb(*pixel))
|
|
||||||
|
|
||||||
|
img.thumbnail((1, 1), Image.LANCZOS)
|
||||||
|
hct = Hct.from_int(argb_from_rgb(*img.getpixel((0, 0))))
|
||||||
opts["mode"] = "light" if hct.tone > 60 else "dark"
|
opts["mode"] = "light" if hct.tone > 60 else "dark"
|
||||||
|
|
||||||
opts_cache.parent.mkdir(parents=True, exist_ok=True)
|
opts_cache.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -99,13 +98,9 @@ def get_smart_opts(wall: Path, cache: Path) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
|
def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
|
||||||
wall = Path(wall)
|
|
||||||
scheme = get_scheme()
|
scheme = get_scheme()
|
||||||
cache = wallpapers_cache_dir / compute_hash(wall)
|
cache = wallpapers_cache_dir / compute_hash(wall)
|
||||||
|
|
||||||
if wall.suffix.lower() == ".gif":
|
|
||||||
wall = convert_gif(wall)
|
|
||||||
|
|
||||||
name = "dynamic"
|
name = "dynamic"
|
||||||
|
|
||||||
if not no_smart:
|
if not no_smart:
|
||||||
@@ -113,7 +108,7 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
|
|||||||
scheme = Scheme(
|
scheme = Scheme(
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"flavour": scheme.flavour,
|
"flavour": "default",
|
||||||
"mode": smart_opts["mode"],
|
"mode": smart_opts["mode"],
|
||||||
"variant": smart_opts["variant"],
|
"variant": smart_opts["variant"],
|
||||||
"colours": scheme.colours,
|
"colours": scheme.colours,
|
||||||
@@ -122,41 +117,20 @@ def get_colours_for_wall(wall: Path | str, no_smart: bool) -> None:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"name": name,
|
"name": name,
|
||||||
"flavour": scheme.flavour,
|
"flavour": "default",
|
||||||
"mode": scheme.mode,
|
"mode": scheme.mode,
|
||||||
"variant": scheme.variant,
|
"variant": scheme.variant,
|
||||||
"colours": get_colours_for_image(get_thumb(wall, cache), scheme),
|
"colours": get_colours_for_image(get_thumb(wall, cache), scheme),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def convert_gif(wall: Path) -> Path:
|
def set_wallpaper(wall: Path | str, no_smart: bool) -> None:
|
||||||
cache = wallpapers_cache_dir / compute_hash(wall)
|
|
||||||
output_path = cache / "first_frame.png"
|
|
||||||
|
|
||||||
if not output_path.exists():
|
|
||||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with Image.open(wall) as img:
|
|
||||||
try:
|
|
||||||
img.seek(0)
|
|
||||||
except EOFError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
img = img.convert("RGB")
|
|
||||||
img.save(output_path, "PNG")
|
|
||||||
|
|
||||||
return output_path
|
|
||||||
|
|
||||||
|
|
||||||
def set_wallpaper(wall: Path, no_smart: bool) -> None:
|
|
||||||
# Make path absolute
|
# Make path absolute
|
||||||
wall = Path(wall).resolve()
|
wall = Path(wall).resolve()
|
||||||
|
|
||||||
if not is_valid_image(wall):
|
if not is_valid_image(wall):
|
||||||
raise ValueError(f'"{wall}" is not a valid image')
|
raise ValueError(f'"{wall}" is not a valid image')
|
||||||
|
|
||||||
# Use gif's 1st frame for thumb only
|
|
||||||
wall_cache = convert_gif(wall) if wall.suffix.lower() == ".gif" else wall
|
|
||||||
|
|
||||||
# Update files
|
# Update files
|
||||||
wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True)
|
wallpaper_path_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
wallpaper_path_path.write_text(str(wall))
|
wallpaper_path_path.write_text(str(wall))
|
||||||
@@ -164,10 +138,10 @@ def set_wallpaper(wall: Path, no_smart: bool) -> None:
|
|||||||
wallpaper_link_path.unlink(missing_ok=True)
|
wallpaper_link_path.unlink(missing_ok=True)
|
||||||
wallpaper_link_path.symlink_to(wall)
|
wallpaper_link_path.symlink_to(wall)
|
||||||
|
|
||||||
cache = wallpapers_cache_dir / compute_hash(wall_cache)
|
cache = wallpapers_cache_dir / compute_hash(wall)
|
||||||
|
|
||||||
# Generate thumbnail or get from cache
|
# Generate thumbnail or get from cache
|
||||||
thumb = get_thumb(wall_cache, cache)
|
thumb = get_thumb(wall, cache)
|
||||||
wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
|
wallpaper_thumbnail_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
wallpaper_thumbnail_path.unlink(missing_ok=True)
|
wallpaper_thumbnail_path.unlink(missing_ok=True)
|
||||||
wallpaper_thumbnail_path.symlink_to(thumb)
|
wallpaper_thumbnail_path.symlink_to(thumb)
|
||||||
@@ -176,7 +150,7 @@ def set_wallpaper(wall: Path, no_smart: bool) -> None:
|
|||||||
|
|
||||||
# Change mode and variant based on wallpaper colour
|
# Change mode and variant based on wallpaper colour
|
||||||
if scheme.name == "dynamic" and not no_smart:
|
if scheme.name == "dynamic" and not no_smart:
|
||||||
smart_opts = get_smart_opts(wall_cache, cache)
|
smart_opts = get_smart_opts(wall, cache)
|
||||||
scheme.mode = smart_opts["mode"]
|
scheme.mode = smart_opts["mode"]
|
||||||
scheme.variant = smart_opts["variant"]
|
scheme.variant = smart_opts["variant"]
|
||||||
|
|
||||||
@@ -184,39 +158,6 @@ def set_wallpaper(wall: Path, no_smart: bool) -> None:
|
|||||||
scheme.update_colours()
|
scheme.update_colours()
|
||||||
apply_colours(scheme.colours, scheme.mode)
|
apply_colours(scheme.colours, scheme.mode)
|
||||||
|
|
||||||
# Run custom post-hook if configured
|
|
||||||
cfg = get_config().get("wallpaper", {})
|
|
||||||
if post_hook := cfg.get("postHook"):
|
|
||||||
subprocess.run(
|
|
||||||
post_hook,
|
|
||||||
shell=True,
|
|
||||||
env={
|
|
||||||
**os.environ,
|
|
||||||
"WALLPAPER_PATH": str(wall),
|
|
||||||
"SCHEME_NAME": scheme.name,
|
|
||||||
"SCHEME_FLAVOUR": scheme.flavour,
|
|
||||||
"SCHEME_MODE": scheme.mode,
|
|
||||||
"SCHEME_VARIANT": scheme.variant,
|
|
||||||
"SCHEME_COLOURS": json.dumps(scheme.colours),
|
|
||||||
"THUMBNAIL_PATH": str(thumb),
|
|
||||||
},
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_random(args: Namespace) -> None:
|
def set_random(args: Namespace) -> None:
|
||||||
wallpapers = get_wallpapers(args)
|
set_wallpaper(random.choice(get_wallpapers(args)), args.no_smart)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user