A modular extension and synchronisation layer around Caelestia. It adds capabilities to the Caelestia Shell, and it carries Caelestia's colour scheme into applications Caelestia does not theme itself.
Note
This is not a fork, a replacement theme engine or a $HOME dump. Caelestia stays the
source of truth for colours: there is no palette here, and every integration translates
what Caelestia already generated for one application. The shell is a patch series
over a pinned upstream revision, and every file belongs to a component you can install
or skip.
- Shell extensions, built as patches and overlays on the upstream Caelestia Shell:
- Text extraction. The screen freezes, you drag over some text, and on release it is on your clipboard. Nothing opens. Tesseract runs locally and the capture is deleted straight away.
- Web search. A dragged rectangle is read with local OCR and only the recognised text is searched, so no image leaves the machine. A freehand circle searches the picture itself with Google Lens, asking first, every time.
- Local AI. A BitNet runtime bound to
127.0.0.1, or any OpenAI-compatible chat-completions endpoint you point it at. It explains, summarises, translates or tabulates captured text, and nothing is sent anywhere unless you press Ask AI. - OCR & AI settings in the shell's own settings app.
- Keyboard shortcuts: an overlay listing what is bound in Hyprland right now, with what each bind does and a search. See Keybinds.
- Desktop profiles: personalities of the same Hyprland and Caelestia session,
such as tiled Caelestia or floating Windows XP.
>themein the launcher, or Theme in settings, switches through one CLI that verifies each switch and rolls back a failed one. See Desktop profiles. - Wallpaper info: right-click a wallpaper in the launcher's
>wallpaperor the Wallpapers settings page to see its size in pixels, on disk and in memory once shown, and to move it to the Trash (gio, after a confirming second click; the wallpaper in use cannot be deleted). The size is read from the file's header, so checking a huge image costs nothing. - Power settings in the shell's settings app: how long the computer may be inactive before it sleeps, and what closing the lid does. See Power.
- UnNova, a system monitor and process manager in the shell's own look: click the character in the power panel. See UnNova.
- Theme adapters, for applications Caelestia does not reach:
- GTK: open GTK windows recolour on a scheme change instead of on their next
start, including Edge and Chrome in their GTK appearance. See
adapters/gtk. - KDE: Dolphin and Ark colours via
kdeglobals, and one font across Qt, GTK and GNOME - Spotify: re-applies the spicetify theme after a scheme change
- Papirus: folder icons follow the scheme
- Cursor: Caelestia's Sweet cursor, rebuilt in the scheme's colours. Upstream
names it
sweet-cursorswhile the package installsSweet-cursors, so stock Caelestia always shows the default cursor. Seeadapters/cursor.
- GTK: open GTK windows recolour on a scheme change instead of on their next
start, including Edge and Chrome in their GTK appearance. See
- Fixes for Caelestia's own app themes that never take effect:
- Zed: Caelestia regenerates a Zed theme on every change, but its settings select One Light and One Dark. This selects the Caelestia theme, which Zed reloads live.
- Firefox: the CaelestiaFox extension colours it live, but Caelestia copies its
layout (
userChrome.css,user.js) only into~/.mozilla, while current Firefox keeps profiles under~/.config/mozilla. This puts them in both.
- Noise-suppressed microphone (optional,
./install --enable voice): PipeWire's WebRTC filter on the default microphone, which removes steady background noise and brings quiet speech up. Dictation and every other application record through it. - Login screen (optional): an SDDM theme that follows the Caelestia scheme,
wallpaper and profile picture. See
system/sddm.
Note
This extends a working Caelestia desktop. Install the Caelestia dotfiles first:
caelestia-shell, caelestia-cli, quickshell and Hyprland.
caelestia-shellandcaelestia-cliquickshell-gitgit,rsync,python,jq- For text extraction:
tesseractwith the language data you want, andwl-clipboard - For OCR language controls:
polkitand a running authentication agent - For web search:
fuzzelto ask before a circle sends anything, andcurlfor the optional file-host mode - For voice dictation:
whisper-cppandwtype - For the local AI model:
uv,git,clang,cmakeandninja, to build the runtime on your machine. Not needed for an external endpoint - For the cursor:
sweet-cursors-git,librsvgandxorg-xcursorgen, plushyprcursorfor a cursor that stays sharp at any scale - For building the shell:
cmake,ninja,libqalculateand the Qt 6 development packages, the same set upstream needs
Each adapter needs only its own application. To see what each component needs and what is missing:
./installer/check_deps.sh
./installer/check_deps.sh shell # also require the shell's build toolsgit clone https://github.com/rugbedbugg/0xIde.git
cd 0xIde
./install --dry-run # exactly what would change, grouped by kind; changes nothing
./install # the default set; nothing in it needs rootTo work with individual components:
./install --list # every component, its default, and whether it is installed here
./install --status # what is installed, and whether it works
./install --only shell # just the shell
./install --enable sddm # add a component that is off by default
./install --disable spotify # skip one that is onDEFAULT in --list is what a plain ./install picks; INSTALLED is what this computer
has now (yes, no, or fix when it is installed but --status reports a problem).
Warning
Components under system/ need root. None is on by default: each prints the exact
change and asks first, and each has a remove that reverses it.
Installing the shell replaces the Caelestia shell that is running, whether that is the stock one or an earlier 0xIde build, with exactly one instance of the new one, and leaves any other Quickshell configuration running as it was. Keep Awake stays on if it was on. Run it from the desktop session.
Re-running ./install is a no-op. An adapter whose application is missing is skipped
rather than failing. The first version of every file this project replaces is kept under
$XDG_STATE_HOME/0xide/replaced/, and nothing here deletes it.
| Key | Action | Result |
|---|---|---|
SUPER + SHIFT + T |
Extract text from a region | clipboard, nothing opens |
SUPER + SHIFT + A |
Search a region, or ask AI about it | browser, or the AI panel |
SUPER + SHIFT + D |
Dictation on or off | each phrase typed where you are |
SUPER + / |
Keyboard shortcuts | every keybind bound right now, searchable |
They are set in overrides/caelestia/hypr-vars.lua and bound in
overrides/caelestia/hypr-user.lua, both extension points Caelestia supports. A binding
that collides with one upstream already made is skipped, with a message, rather than
shadowing it.
Keyboard shortcuts lists the keybinds Hyprland has registered at the moment you open
it, not a list kept here, so a binding that was skipped never shows. Open it with
SUPER + / (kbShowShortcuts), from Keyboard Shortcuts in the launcher, or with
caelestia shell shortcuts toggle; type to search by key or action, and press the key
again, Esc or click outside to close it. Like the others, its key is skipped if
something already uses it, which leaves the launcher entry.
SUPER + SHIFT + A has three choices, picked from a card at the bottom of the selector
before you drag: Text searches the words read from a rectangle, Image sends a
circled region to Google Lens after asking, and Ask AI opens a rectangle's text in the
AI panel. It opens on Text, the one that uploads nothing, and the card hides once you
start dragging; press Esc to start over.
- Choose a backend, once. In the shell's settings, open OCR & AI and pick Local to run a small model on this computer (see below), or point it at any OpenAI-compatible endpoint.
- Capture. Press
SUPER + SHIFT + A, choose Ask AI, and drag over the text. - Ask. Pick Explain, Summarize, Translate, Custom or Code and press Ask AI. Any text you select in either pane is what gets asked about instead of the whole capture.
The answer appears beside the text, formatted, with code in the terminal's font; it can be scrolled while it is still being written. Code asks for the minimal working code for the problem in the capture.
The first answer takes a few seconds while the local model starts. It is a 2B model on the CPU, good for short passages rather than long documents. With the local backend nothing leaves this computer.
Translate runs offline, without the model, once two or more languages are installed
under Translation in the OCR & AI settings (about 160 MB each). Pick the languages
beside the Translate button; any two installed languages translate into each other through
English. Without them, Translate asks the model instead, which is weak outside English.
The models are Argos Translate's, run
with CTranslate2; manifests/translate.toml pins them.
For scripts, or a key of its own, the same panel is reachable over IPC and as the
caelestia:askAi shortcut:
qs -c caelestia ipc call picker openAskAi # capture, then ask
qs -c caelestia ipc call picker showText "$(wl-paste)" # ask about text you already haveThe backend can also be set under ai in shell.json.
The local backend runs Microsoft's
BitNet b1.58 2B4T, a
2-billion-parameter model with ternary weights, on
BitNet.cpp. Neither is shipped here:
manifests/ai.toml pins both revisions and the model's SHA-256.
Install it from OCR & AI in the shell's settings, or say yes when the AI panel offers it the first time you ask. If Download cannot be pressed, the reason is shown under it: missing build tools, too little disk, or the check still running. It then:
- checks for the tools above and about 4.1 GiB of free disk,
- fetches the pinned BitNet.cpp source,
- builds it for your CPU, which takes several minutes,
- downloads
ggml-model-i2_s.gguf, about 1.1 GiB, and verifies its hash, - starts the model once as a health check.
Both places show the current step, the time spent on it, and a progress bar: the
downloaded bytes while downloading, a moving bar for steps that report no amount. It keeps
going if you close the panel, and the AI panel shows it in its banner. A failure shows the
installer's own error, the step, and install.log; Cancel stops it and keeps nothing.
Everything goes in $XDG_DATA_HOME/0xide/ai (~/.local/share/0xide/ai), and Uninstall
on the same page deletes it. From a terminal, the same installer is:
cd ~/.config/quickshell/caelestia/assets/ai
uv run --no-project --python 3.13 runtime.py installThe model server listens on 127.0.0.1 only, starts on the first question, and stops
after five idle minutes. A restarted shell, crash recovery included, answers from the
server that is already running; one no shell asks for stops after the same five minutes.
SUPER + SHIFT + D turns dictation on; press it again to turn it off. While it is on,
everything you say is typed into the focused window, a phrase at a time, as soon as you
pause. Speech is transcribed on this computer by
whisper.cpp, and no recording is kept. A muted
microphone is refused rather than recorded as silence. It needs the whisper-cpp and
wtype packages, and the speech model, which Voice dictation in the OCR & AI settings
installs (148 MB, pinned in manifests/speech.toml). It listens
for English unless you set another language there, or auto to detect it.
One Hyprland/Caelestia runtime. Multiple desktop personalities.
Every profile runs on the same Hyprland session with the same Caelestia shell. What a profile changes is how windows are managed, and later how the shell looks:
| Profile | Window policy | Presentation |
|---|---|---|
caelestia |
tiling: Hyprland's tiling, as always |
Caelestia |
windows-xp |
stacking: every window floats and overlaps |
not built yet; Caelestia's look |
A third policy, hybrid, tiles windows except the classes and tags a profile lists in
wm_float_classes and wm_float_tags. Policies are in
orchestration/wm-policies/.
Type >theme in the launcher and pick a desktop, or use Theme in the settings app.
The same switch from a terminal:
./0xide profile list # * marks the active one
./0xide profile plan windows-xp # what switching would do; changes nothing
./0xide profile set windows-xp # switch
./0xide profile set caelestia # and back
./0xide profile status # is the recorded profile really in effect?A switch records the window layout of the profile it leaves, applies the new policy to
open windows and to windows as they open, puts back the windows the new profile's own
snapshot knows, and checks that the shell runs, owns notifications, and the windows are
as asked. Only then is the profile recorded as active. If any step fails, the previous
profile's policy and window layout are put back and nothing is recorded; the details are
in $XDG_STATE_HOME/0xide/desktop-profile.log. set exits 0 on success, 1 when it failed
and the previous desktop was restored, 2 when that restore failed too.
Returning to Caelestia tiles again every window a policy floated, puts windows that existed before the switch back on their workspaces, and leaves windows that were already floating alone. The order of tiled windows within a workspace is not restored. Windows on special workspaces, fullscreen and pinned windows are never touched. A window is matched by its address, Hyprland's stable id and its class together, since Hyprland can give a closed window's address to a new one; a window that was closed and reopened is treated as new, and one without a stable id is left as it is.
At login, Hyprland runs ./0xide profile resume, which gives new windows the recorded
profile's policy. A missing or damaged record means Caelestia.
Note
The Windows XP profile changes window management only. Its Luna look, Start menu and taskbar are not built yet, so the Caelestia shell keeps its own look. XPlasma, which restyles a KDE Plasma session, was studied as reference material for that look; it is not used at runtime, and 0xIde never starts Plasma.
In the shell's settings, Power → Sleep after inactivity sets how long the computer may go without keyboard, mouse or touch input before it suspends: 5, 10, 15 or 30 minutes, 1 or 2 hours, a custom time on hour and minute drums (five-minute steps), or Never. Any input starts the count again. Keep Awake in the Utilities drawer, media that is playing and applications that ask to keep the screen on all pause it, as they always have.
It is the suspend entry of general.idle.timeouts in Caelestia's shell.json, the list the
shell's own idle monitors already run, so it applies at once, survives restarts and needs
no other idle service. Locking and turning the display off are separate entries and stay
as they are; Never turns off only the suspend, and their timers keep running. A custom
idle action is kept with its fields, and only its time changes. If more than one entry
suspends, the page says so and changes none of them; keep one in shell.json to set it
here.
Note
general.idle.timeouts is a single setting, so the first change writes the whole list to
shell.json, including upstream's lock and display-off entries as they are now. From then
on, a later Caelestia release that changes those defaults does not change them for you.
Edit or remove general.idle.timeouts in shell.json to take upstream's defaults again.
Power → Lid sets what closing the lid does: Suspend, Do nothing, Lock screen, or Hibernate where the computer can hibernate (logind says so; a swap on zram alone cannot hold an image). It is set twice: for the lid on its own, on battery and on a charger alike, and for the lid closed with an external display connected or when docked, which stays Do nothing until you change it. systemd-logind acts on it, not the shell, so it holds while the shell is stopped or restarting.
Changing it needs ./install --enable lid, which installs one root helper and a rule for it
alone; until then the page shows what logind does and how to change it. The helper writes
only /etc/systemd/logind.conf.d/80-0xide-lid.conf, reloads logind in place (the session
keeps running) and reads the setting back from logind, putting the previous file back if
anything fails. A drop-in that sets the lid after it, such as a vendor's, wins over it:
the page then names that file and changes nothing. An application holding logind's lid
inhibitor, which a docking tool may, is named too. See system/lid.
Click the animated character in the power panel (or run
qs -c caelestia ipc call unnova open). There is only ever one UnNova window; opening it
again brings the open one forward. It has two tabs.
0xIde shows what 0xIde itself runs, by what it is rather than by process ID:
| Component | Shown | Actions |
|---|---|---|
| Local AI | stopped, starting, ready or stopping; its memory and CPU | Start, Unload, Restart |
| Dictation | the speech model, and whether dictation is listening | none |
| Translation | installed languages, and whether it is translating | Cancel active translation |
| Text recognition | languages, and whether it is reading a capture | none |
| Desktop | the active desktop profile | none (Theme does) |
Only the local AI keeps a server running, so only it has something to unload or restart; those use the runtime's own stop and start. The others start a helper for each use and leave nothing behind to manage. You can cancel an active translation. Model and language setup stays in Settings; dictation and capture keep their existing shortcuts. Opening UnNova never starts the local AI; Start does so explicitly when its model is installed.
Processes lists every process you can see, calmer than a task manager's table: name, CPU and memory, with PID and user underneath. Search matches the name, PID, command line or user. List sorts by memory, CPU, name or PID, and values that barely differ keep their places instead of reshuffling every second. Tree shows each process under its real parent; clicking a row selects it, and only its chevron folds it. The selected process fills the left pane: identity, CPU and memory with a minute of history, threads, how long it has run, state, parent, executable and command line.
End task sends SIGTERM, which asks the process to exit and lets it clean up. If it is still running a few seconds later, UnNova says so and offers Keep waiting or Force stop (SIGKILL); it never escalates by itself. Actions… also offers Force stop, Pause (SIGSTOP), Resume (SIGCONT), Hang up (SIGHUP), Interrupt (SIGINT) and any other signal by name, each explained before it is sent. Processes such as init, Hyprland, the shell itself, PipeWire and networking say what ending them would break. Nothing is elevated: a process you are not allowed to signal says so.
A process is identified by its PID together with its start time, because Linux reuses PIDs. On supported kernels, before any signal UnNova pins the process with a pidfd and checks the start time again. This prevents a reused PID from redirecting a signal to another process. Exactly the selected process is signalled, never its children or others with the same name. An identity mismatch always refuses the action.
UnNova reads /proc directly, once a second, and only while its window is open. It starts
no processes to do it.
CPU percentages are a share of total machine capacity. Memory in the list is resident
memory minus shared pages; the details also show full resident memory. These are /proc
estimates, not proportional-set-size accounting. Command lines are capped at 64 KiB.
On kernels without pidfds, signalling falls back to rechecking PID and start time before
kill(2); that fallback cannot eliminate the narrow exit/reuse race between those calls.
cd 0xIde
git pull
./installTo move the shell to a newer upstream revision:
$EDITOR shell/upstream.pin # 1. point the pin at the new revision
./shell/build.sh # 2. rebuild; this stops if a patch no longer applies
./tests/run # 3. re-apply every patch to a clean checkoutA patch that no longer applies must fail. Nothing here resolves a conflict
automatically, because a silently mis-rebased patch is worse than a refused build. Rebase
it by hand in build/shell-src with git apply --3way, then write it back as a
git format-patch series.
| File | What it sets |
|---|---|
overrides/caelestia/hypr-vars.lua |
Hyprland variables: browser, editor, cursor theme, the keybinds above |
overrides/caelestia/hypr-user.lua |
Hyprland settings and binds that are not plain variables |
overrides/caelestia/shell.json |
Caelestia Shell settings, including the AI backend under ai |
overrides/caelestia/cli.json |
caelestia-cli settings, including enableGtk for the gtk adapter |
overrides/fish/user-config.fish |
fish additions |
overrides/foot/overlay.conf, overrides/starship/overlay.toml |
keys merged into two files that have no include mechanism |
overrides/zed/overlay.jsonc |
the theme key merged into Zed's settings.json |
$XDG_CONFIG_HOME/0xide/region-search.conf |
region search mode and confirmation |
$XDG_CONFIG_HOME/0xide/adapters.enabled |
which adapters the theme hook runs |
profiles/<id>/profile.conf, $XDG_CONFIG_HOME/0xide/profiles/ |
desktop profiles: provider, window policy, presentation |
In OCR & AI → Text recognition, Languages to use shows the installed codes.
Leave the field blank to use them all, or enter codes joined with +, such as
eng+deu+fra. The next two rows install or remove a language by name or code when you
press Enter. They manage Arch's tesseract-data-* packages through an administrator
prompt and refresh the list automatically. The last recognition language and
Tesseract's orientation data are kept. Packages installed outside pacman must be
managed separately.
ai.ocrLanguages in shell.json stores this selection. config.local at the
repository root, never committed, overrides paths and binary names for a machine
that puts things somewhere unusual.
- Text extraction is local. The recognised text goes to the clipboard and nowhere else, and the capture is deleted whether it succeeded or not.
- Region search stays local. Only the recognised text is searched.
- Circle search goes to Google Lens, and asks first. The capture is sent to Google
Lens itself and its results open in your browser: only Google receives it, and no
public link is made. Every send asks first, and a declined, failed or dismissed
prompt refuses.
mode="host-upload"inregion-search.confroutes it through a public file host instead,mode="text"keeps both gestures local, andmode="off"disables region search. - The AI backend defaults to a local model on
127.0.0.1. A remote endpoint is your choice, and the destination is shown above the submit button. - No model is in this repository.
manifests/ai.toml,translate.tomlandspeech.tomlpin what is fetched, and each is downloaded only when you ask. - Dictation stays on this computer. Audio goes from the microphone to a local
whisper-server on
127.0.0.1and is not kept. - Nothing that needs root is on by default, and sudo rules are checked with
visudo -cbefore they are installed.
Caelestia
├── supported overrides hypr-vars.lua, hypr-user.lua, shell.json, cli.json
├── shell patches/overlays over a pinned upstream revision
└── theme state scheme.json, written by Caelestia
└── orchestration theme.postHook runs hooks/post-theme
└── adapters one directory per application
orchestration/hooks/post-theme holds no application knowledge: it reads the enabled
list and runs each adapter's apply. Adding an application means adding a directory
under adapters/, never editing the hook.
| Where | What goes there |
|---|---|
shell/patches/ |
changes to files upstream owns, as a git format-patch series |
shell/plugin/ |
C++ the packaged plugin lacks, with its registration patches |
shell/extensions/<name>/tree/ |
new files, overlaid onto the upstream tree |
adapters/<name>/ |
one application: adapter.conf, apply, optional install, status |
system/<name>/ |
anything privileged; it prints the change and asks |
profiles/, providers/ |
desktop profiles, and the provider that shows and verifies each one |
orchestration/wm-policies/ |
window-management policies a profile can name |
Prefer an overlay to a patch: a patch is only for a line that must change inside a file upstream maintains, and each one costs something at the next update.
./tests/runThe suite checks syntax and metadata across the tree, re-applies every patch to a clean
checkout of the pin, and drives the installers, adapters and privileged helpers with
their side effects stubbed. It fails if generated state (the built plugin, the upstream
checkout, the AI model, Caelestia runtime state) ever becomes tracked. It never reaches the
caelestia CLI, and fails if the GTK theme, colour scheme, icon theme or a browser theme
policy of the desktop it runs on has changed by the end. A qs harness that loads the
shell's services needs a current wallpaper in its private state; without one, Wallpapers
applies its fallback through the CLI, which themes the real desktop. CI runs the
suite on Arch Linux and shellcheck on Ubuntu for pull requests, pushes to main and
ci/**, and manual runs.
UnNova's process core is compiled on its own with c++ and signals only children the test
starts. Its service and the local AI lifecycle are driven in a windowless qs; those two
parts need qs and a plugin built with ./shell/build.sh --no-install, and are skipped
without them. The runtime-control checks use mock services in an unmapped window
(./tests/unnova-live --controls); they need a Wayland connection and never start models
or translation requests.
CI compiles the process core; the full plugin and Quickshell checks require the local
build environment described above. Qt's model-contract test covers repeated row
insertions, removals and moves. For the opt-in rendered check, run ./tests/unnova-live
in a Wayland session after building. It opens an isolated UnNova with private settings,
signals only its own disposable children,
and records screenshots, logs, sampling times, CPU use and RSS in a temporary directory.
It neither installs the build nor restarts the running shell.
This repository used to hold my acrylic Waybar, Rofi and Kitty dotfiles. They were
retired when the desktop moved to Caelestia, and they are still in the history: every
commit up to [Repo]: Retire the Arch dotfiles for the Caelestia setup has them.
Use the overrides in overrides/, which are the extension points Caelestia supports.
Nothing here edits a file the Caelestia dots deploy.
That needs a RemoteDesktop portal, which xdg-desktop-portal-hyprland does not provide.
One local portal that did is documented but deliberately not distributed; see
system/portals.
Remove its line from $XDG_CONFIG_HOME/0xide/adapters.enabled.
Built on Caelestia by @soramanew, and on Quickshell by @outfoxxed. The rectangle-or-circle region search follows the one in end-4's dots.
GPL-3.0-or-later. Caelestia Shell is GPL-3.0 and this repository contains patches against
it, so a permissive licence is not available for the whole. See LICENSE.