Commit Graph
16 Commits
Author SHA1 Message Date
2c247e390e Stop the About logo moving when the animation starts (#7786)
* Ask the terminal whether the layout scrolled

Whether the logo is on the rows the frames address was decided by arithmetic: count the lines of a second fastfetch run, and animate if the window has one more. That holds only while the second run and the real one agree, and they need not — a module that appears between them, a wrap the measurement never sees, anything that makes the painted layout a row taller than the counted one. The screen then scrolls, the logo sits a row above where these frames draw it, and the animation paints a copy of the logo one row below the real one.

fastfetch has just painted when the question is asked, so the terminal already knows the answer: an unscrolled layout leaves the cursor one row past its last line. Ask for it, and keep the arithmetic for a terminal that will not say.

The reply has to be read without a command substitution around it, or the query goes into the substitution's own pipe instead of to the terminal, and what comes back is the escape rather than a row.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Find the logo in the render instead of assuming where it is

The row and column the frames draw on came from the padding in this repo's fastfetch config. The config that runs is the one in /etc, and `omarchy dev link` says plainly that it does not replace files at fixed system paths — so the two can disagree, and nothing in the code would notice. A logo fastfetch drew on row 1 or row 5, redrawn on row 3, is a logo that jumps the moment the animation starts and stays where the animation put it.

fastfetch's own output is already being captured to measure the layout's height, so the logo can be found in it: take the longest line of the file as a landmark, look for it in the render, and turn where it was found back into where the logo starts. Padding of any size lands correctly, and a render that does not contain the file's own text is not this logo at all — a config that restyled it, a placeholder that was substituted — so there is nothing to animate and it is left alone.

That last part subsumes what the config-path check was guessing at. It is kept, because it also decides whether the window may be fitted, which happens before there is a terminal to measure anything in.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Take the cursor query back out

Asking the terminal where the cursor came to rest was meant to see a scrolled layout rather than infer one. It cannot: a linefeed at the bottom margin scrolls the screen and leaves the cursor on the bottom row, so a render that fitted exactly and one that scrolled both answer with the same number. Measured on a real terminal — a 29-line layout in windows of 30, 29 and 28 rows answers 30, 29, 28. The guard therefore only ever agreed with the arithmetic it replaced, and only in the case the arithmetic already had right.

What it did add was a read on the same stdin the keypress that closes About arrives on. A key pressed while the query is outstanding is swallowed; a reply that arrives after the timeout is read by the next tick as the keypress, and the window closes on its own; typed text of the right shape is accepted as an answer, because nothing checked for the introducer; and the query is written before the read turns echo off, so a fast reply can be echoed onto the screen.

None of that buys anything the line count did not already give, so it goes.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Fit the About window with a little room to spare

The window was sized to exactly what was measured: the columns the widest module line needed, and one row past the layout's last line for the cursor. That measurement is taken once, and then the content goes on living. An uptime turns minutes into hours and hours into days, a version string grows, a module shows up after the next boot — and a window with nothing in hand clips at the right edge or scrolls the top padding away the moment any of it happens. A scrolled layout also moves the logo off the row it was drawn on, which is where a shifting logo comes from.

Two columns and a row, which is invisible on screen and enough for anything that ticks over. The remembered size in about.fit is keyed on the logo, so the first launch after this still opens at the old size and the fit corrects it there and then.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 20:18:14 +02:00
eca89f9518 Animate the About logo with a passing glint (#7768)
* Animate the About logo with a passing glint

fastfetch has no animation of its own, so the sweep is ours. Every frame is composed once up front and a tick writes one of them, repainting only the cells fastfetch drew the logo on, which holds 40fps for under 1% of a core and never reaches the module column six columns to its right.

The logo is handed back exactly as it arrived: frames carry the colour fastfetch drew it in rather than a plain reset, so between glints the window is byte-for-byte the one it was before this change. Where the logo on screen might not be the text in the file, it stays still rather than guess — a fastfetch config in any directory searched ahead of Omarchy's own, a logo built from $1 colour placeholders or from a tab fastfetch expands itself, a window too small for the layout to fit without scrolling, and a shell whose locale is counting bytes instead of characters.

A resize moves those cells, so a WINCH trap ends the sweep on the frame it happens rather than up to a poll later, which measured 10ms against 830ms. The polling stays as the backstop for a signal that arrived while it could not be taken.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Give the sheen a file of its own

The launcher was carrying two things: what a glint looks like, and what it is drawn over. Only the second is about fastfetch. bin/omarchy-logo-sheen now knows how to lean a band of light across an ASCII logo and nothing about About, and it is handed where the logo sits, what colour to give its cells back, and how much room it has. When a frame may be written stays with the launcher, because that is inseparable from how the window closes and resizes.

The tests split along the same seam, and the sheen's half no longer strips the launcher's tail to reach the code it tests.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Leave a logo still when one character is not one cell

The frames slice the logo by character and the terminal draws it by column, so the sheen only puts a row back where it found it while those two agree. A double-width glyph, a combining mark and a joined emoji each break that, and a cut through one of them renders as something else — pushing the rest of the row right, into the module column, with nothing on screen to say the logo moved. A user can put any of the three in the logo by editing the text.

Comparing the row's character count against its width in columns is the assumption itself, so it is what gets checked, rather than a list of the glyph classes that happen to break it today.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Size the About window from the layout fastfetch drew

The fit predicted the content height from the logo and the module column, taking the taller of the two. Once the logo is the taller one fastfetch writes a row more than that arithmetic expects, so every logo of 27 rows or more got a window one row short and scrolled its top padding away. The shipped logo is 26 rows, which is why it never showed.

Ask fastfetch how tall the layout came out instead of predicting it. That measurement was already being taken for the sheen, which refuses to animate a layout that scrolled — so a tall logo used to lose the glint as well as the padding row.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Leave the logo still when NO_COLOR asks for none

fastfetch drops the logo's colour when it writes to a terminal that set NO_COLOR, but not when it writes to the substitution that measures it. The colour to hand those cells back in was therefore measured as green while the logo on screen had none, and the first glint would have left the whole logo green. It stops suppressing autowrap there too, so a narrow window soft-wraps and scrolls where the measurement cannot see it and the frames go on addressing rows that moved.

Both follow from animating a screen drawn in a mode the measurement did not reproduce. A glint is colour besides, which is the thing NO_COLOR asks for none of.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Read fastfetch's config paths whole, and do not read silence as an answer

Splitting each listed path on whitespace dropped everything after the first space, so a config under a home directory containing one was missed and the fit and the sheen went ahead against a layout neither had measured. The marker fastfetch prints beside the config it settled on is not part of the path either.

A listing that failed was also indistinguishable from one that found nothing, because the status of a process substitution is discarded, so a fastfetch that could not enumerate read as "no custom config". Fall back to the directory fastfetch has always looked in first rather than take silence for an answer.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Notice a resize that lands while the grid is being read

The sweep read the WINCH flag before the grid rather than after it. A signal arriving during the two command substitutions that read the grid is delivered only once they finish, so the flag was still false when it was tested, the grid still compared equal, and one more frame went out at coordinates the resize had already moved. Read the flag last, after the check it could have landed during.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Check that the render loop is what plays the sheen

Every builder the tests drive can be exercised while nothing on screen ever animates. Replacing the render loop's animated branch with the still one it replaced left all of them passing, so nothing was holding the animation onto the screen it belongs on.
🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>

* Name the animation after the branding it animates

omarchy-logo-sheen sat in a group of its own, and a group whose only command is hidden still gets a header printed for it with nothing underneath. It belongs beside omarchy-branding-about, whose art it animates: the group already exists, the two halves of About branding are next to each other, and `omarchy branding about` still routes to the command rather than the helper now sharing its prefix.

The tests take the names of what they cover, the way the rest of the suite does.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fewer moving parts in the sheen

Four questions about a logo turned out to be one. Whether the shell is counting characters or bytes, whether a tab or an escape is in the line, whether a glyph is double-width or a combining mark or a joined emoji — each was asking whether one character is one cell, so that is the only thing asked now. It is also less strict in the one case that deserves it: plain ASCII art animates in a C locale, where the locale probe used to refuse everything.

One band of light instead of a white core inside a green halo, which is three colour spans a row rather than five and no helper to clamp four cuts with. Only the left cut needs clamping at all: a slice starting past the end of a line is already empty, while a negative offset would count from the end of it.

One loop instead of two, because an empty frame list plays nothing. A logo that cannot be animated now waits in the loop the animated one rests in rather than in a second copy of it, and the build that failed leaves no frames behind, since that loop plays whatever it finds.

The logo's colour comes out of one match rather than a loop eating one escape at a time.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Say in the manual which art the glint leaves alone

The line promised the glint to any art in the file, and the code does not: art whose characters are not one column wide is left still on purpose, as is the whole screen for anyone keeping a fastfetch config of their own. Somebody reading the old line and seeing a still logo would take deliberate behaviour for a broken feature.

Name the condition rather than the list of glyph classes behind it, and say that both modes of Set From Image meet it, because that is the path almost everyone is on.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Draw the band in a colour bold cannot collapse

A terminal can be told to render bold text in a brighter colour — foot's bold-text-in-bright, whose palette-based form brightens a bold regular colour into its bright counterpart. fastfetch draws the logo bold green, so under that setting the logo is already bright green, which is the colour the band was using: the glint came out the same green as the art and nothing appeared to happen at all. None of the four terminal configs set it, so it was waiting on whoever turned it on.

Bright white instead, because no regular colour brightens into it, and the band shows whatever the terminal does with bold. Narrower with it, since white against green needs less width to read than a second green did.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 17:19:23 +02:00
07fccef41c Add Super + Q as a second chord for closing a window (#7767)
* Add Super + Q as a second chord for closing a window

Super + W stays the documented default. Super + Q is the chord people
arrive with from macOS, where Command + Q quits the app, and typing it
into Omarchy did nothing at all until now.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

* Put an action's alternative chord on one keybindings row

Super + W and Super + Q both read "Close window" in the menu, two rows
apart, with nothing to say they were the same thing -- and the
alternative sorted above the default. The scratchpad and the calculator
had the same trouble, each bound to a chord and to a second key.

Four actions are named as having an alternative, one at a time, and the
second chord joins the first one's row. A rule would be wrong here: Alt +
Tab and Shift + Alt + Tab both say "Reveal active window on top" while
cycling opposite ways, and a media key is nobody's idea of an alternative
to a Super chord. Both halves still have to agree on what they dispatch,
since a label is only what a chord is called, and an unresolved
dispatcher never counts as agreement.

Nothing is allowed past the 35-character column: a pair that would
overrun it stays as two rows rather than pushing its arrow out of line.
The menu elides a row that outgrows its card -- 754px of label, 78
monospace characters at the heading size -- and the longest entry already
sits at 74, so widening the column to fit the widest pair would have cost
two dozen rows the end of their description.

Priority ordering reads the rendered row, so the chord sharing it would
otherwise reclassify the entry: XF86Calculator alone belongs in the tail
kept for media keys, and it took the calculator down there with it.
Ranking now reads the chord that leads the row.

The key left of 1 reads as ~ rather than Hyprland's name for it, whether
a bind names it or reports the keycode for the keymap to resolve. Cached
records predate all of this, so the cache version moves with it.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-authored-by: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 16:55:01 +02:00
13a969e1ab Only offer Update > Extra Themes when there is one (#7775)
* Only offer Update > Extra Themes when there is one

omarchy-theme-update pulls the themes under ~/.config/omarchy/themes that came from a git clone, so on a machine that has never installed one by hand the row opens a terminal that prints nothing and closes. Guard it with the same predicates the command itself applies, since a row that shows over a symlinked theme or a worktree's `.git` file is the same dead end in a narrower shape, and pin the two to each other in the guard test.

Co-Authored-By: Codex XHigh <noreply@openai.com>

* Extract the Extra Themes guard into omarchy-theme-extras

The row's `when:` and omarchy-theme-update each carried their own idea of which themes came from a git clone, and the two only matched because a test held them together. Name it once instead: omarchy-theme-extras lists those directories and exits nonzero when there are none, so the row asks exactly the command its action runs. Living in a script also puts the glob out of reach of whatever shopt a login shell left set for the guard batch.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-22 16:24:21 +02:00
2c593dbbaa Switch back to the packaged quickshell now that 0.3.1 kills synchronously (#7769)
* Switch back to the packaged quickshell now that 0.3.1 kills synchronously

Omarchy shipped the quickshell-git build for a single fix: 0.3.0's `kill` returned before the instance had exited, so the kill loop in omarchy-restart-shell could race a dying shell. Upstream 0.3.1 ships that fix, which makes extra/quickshell the better package to be on again — signed, versioned, and not rebuilt from a moving branch on every update.

The migration swaps unconditionally instead of first checking which version the mirror offers. A machine left holding quickshell-git while the shipped package list names quickshell has no way to reconcile the two: omarchy-reinstall-pkgs installs that list with --needed, which does not skip a name that is not installed, and the conflict it then walks into has no answer under --noconfirm. A mirror that is briefly behind installs 0.3.0 instead and the next upgrade carries it to 0.3.1, which is much the cheaper way to be wrong.

🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh.

Co-Authored-By: Codex XHigh <codex@openai.com>

* Drop the quickshell version note from the shell restart loop

The comment qualified the kill loop as needing 0.3.1 or newer, but omarchy-restart-shell ships in the same package upgrade that brings quickshell along, so a machine running this code already has the version the loop depends on. The caveat could never be false where it was read, which left it as version archaeology rather than something the code could not say for itself.

🤖 Generated by Opus 5 in Claude Code.

---------

Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-22 15:02:12 +02:00
d3d9bea1ee Add a clock format with live seconds (#7586)
* Add a clock format with live seconds

Right-clicking the clock now reaches "Thursday 09:39:23" and its AM/PM twin, and the widget's SystemClock ticks once a second only while a format that prints seconds is showing — every other format keeps the minute precision it had, so nobody pays for a repaint a second to read a label that changes once a minute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Read an unterminated literal in a clock format as text

Qt reads an opening quote with no closing one as a literal running to the end of the format, so "HH:mm 'sec" prints "09:39 sec" and never a second count — but the seconds test stripped only balanced quotes, saw the s, and put the widget on a per-second tick for a label that changes once a minute. The wiring assertions went the other way: each passed while the feature was broken, so hard-coding showsSeconds to false, dropping the label's onDateChanged, or commenting the precision line out and leaving the text behind all shipped green. Comments now come out of the source before it is matched, and both halves of the tick are asserted.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-20 11:44:11 +02:00
OmarchybotandGitHub 688d7df5d2 Drop the Windows VM start notification (#7585)
The shell already shows a "Launching Windows…" OSD from the moment the desktop entry is activated until the RDP window appears, so the notification duplicated feedback the user is already looking at. The failure notification stays: nothing else reports a VM that never came up.
2026-08-20 10:48:47 +02:00
023021ad2d Add Remove > AI for the apps Install > AI offers (#7504)
* Add Remove > AI for the apps Install > AI offers

Install > AI grew five entries and Remove grew one: Dictation, sitting on its own at the top level. Everything else installed from that menu had to come back out through Remove > Package by name.

Mirror the install tree instead. Dictation moves under the new AI submenu where its installer already lives, and ChatGPT Desktop, Grok Bot, LM Studio, Ollama and T3 Code get removers beside it. Each entry is conditional on the thing actually being installed, so the submenu only ever lists what is there.

What each remover deletes was read off a machine that had all five installed and launched, not guessed. That matters most for T3 Code, which bootstraps the agents it drives: ~/.claude.json, ~/.grok, ~/.npm and ~/.local/share/opencode all appear the first time it runs, and all of them outlive it, so it takes only ~/.config/t3code and ~/.t3. Grok Bot is the same trap in miniature -- ~/.grokbot is its own, ~/.grok belongs to the Grok CLI.

Ollama drops every acceleration variant rather than the one the installer happened to pick, and disables the service before the package, since that is what holds the models open.

🤖 Generated by Opus 5 in Claude Code.

* Remove only what these apps own, and only where removal works

Three defects from an independent review of the previous commit.

ChatGPT Desktop was deleting ~/.cache/codex-runtimes, which belongs to the Codex CLI rather than the desktop app: the `codex` binary resolves its runtime and plugins out of that directory, and it ships in a package this remover does not touch. Removing the desktop app took the CLI's prepared runtime with it, leaving a separate, still-installed tool to rebuild it -- and unable to, offline.

Ollama's row appeared whenever the `ollama` command existed, but omarchy-pkg-drop removes exact package names. With ollama-bin, ollama-git or a hand-built binary the entry offered a removal it could not perform: the service went down, /var/lib/ollama and ~/.ollama were deleted, and it reported success with the program still installed. Every acceleration variant depends on the base package, so testing for that package covers each one the installer can produce and nothing it cannot remove.

LM Studio keeps its models under a relocatable home, and ~/.lmstudio-home-pointer is the only record of where they went. The remover deleted the pointer and the default path, so a user who had moved their models kept every one of them while being told they were gone. Read the pointer before deleting it, and refuse one aimed at / or at the home directory itself, since following it there would take everything.

Co-Authored-By: Codex XHigh <noreply@openai.com>

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-19 17:29:36 +02:00
OmarchybotandGitHub 260a729104 Add T3 Code to the AI install menu (#7496)
t3code-bin is in the Omarchy repo now, so the menu can offer it the way it offers Cursor and Grok Bot: install the package, then launch the desktop entry it ships.

The mark is a trace rather than a download. T3 publishes no monochrome SVG — the app icon is a black rounded tile with the letters knocked out of it, and a tile flattens to a solid square once the menu recolors every path with the theme foreground. Tracing the lettermark out of that icon keeps the silhouette that actually reads.

The font is package-owned, so the glyph reaches a desktop through an omarchy-settings release rather than omarchy update. Until that release lands, a pulled checkout draws the entry with no icon.

🤖 Generated by Opus 5 in Claude Code.
2026-08-19 16:22:46 +02:00
3765e8010b Switch DNS providers without a password prompt (#7472)
* Switch DNS providers without a password prompt

The network panel and the menu run omarchy-dns from a process with no
terminal, so require_root reached for pkexec and put a polkit password
prompt in front of what is meant to be a one-click toggle.

Grant %wheel passwordless sudo for the three stock providers and take
that path whenever the grant covers the invocation. Custom stays out of
the grant: it points the machine at servers the caller supplies, and it
already runs in a terminal that can ask.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Pick the elevation path without asking sudo

The `sudo -n -l` probe answered the wrong question. It reports whether a
command is permitted, not whether it is passwordless, and the %wheel rule
every Omarchy install ships permits everything -- `sudo -n -l /usr/bin/rm
-rf /tmp/x` exits 0. So the probe passed for Custom too, and the exec
below it ran `sudo -n`, which fails outright with no terminal and no way
back to pkexec.

Decide from what the sudoers rule actually says instead: sudo when there
is a terminal to type into, or when the resolved path and the provider
are both ones the rule names. Everything else keeps going through polkit.

Pin a root-owned PATH once elevated, too. `omarchy dev link` puts a
user-writable checkout ahead of sudo's secure_path for every command, so
a passwordless grant on a script that resolves nmcli, tee, and install
through PATH would otherwise hand root to whoever can write there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Keep users outside %wheel on the polkit path

The rule grants %wheel, so path and provider alone do not mean sudo will
take it. A user outside the group was sent to sudo anyway, and with no
terminal to answer the prompt that is a dead end -- polkit at least
offers to authenticate as somebody else.

Two holes in the test alongside it: it accepted any file containing the
expected rule, so a second, argument-free line would have widened the
grant unnoticed, and run as root it would have sailed past the stubs and
rewritten the host's own DNS config.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Elevate the system install, whatever copy was invoked

The rule names /usr/bin/omarchy-dns, so a dev-linked checkout handed sudo
a path nothing could match and fell back to a polkit prompt. Re-exec the
packaged path instead: the privileged half is the system install
everywhere, the grant matches everywhere, and the path comparison and the
PATH pinning that existed to work around the checkout both go away.

Dev-linked checkouts run their own unprivileged half and the installed
one as root, which is the trade for not carrying a second code path.

---------

Co-authored-by: Omabot <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:28:56 +02:00
a4219f8f4a Store theme backgrounds as webp (#7477)
* Decode webp in the shell

The background and the lock screen are drawn by Quickshell, so they
decode through Qt, which ships handlers for png, jpeg and gif but not
webp. QImageReader answers "Unsupported image format" and the layer
comes up blank. Any third-party theme shipping a .webp background hits
this today, even though every path that goes looking for a background
already globs the extension.

qt6-imageformats supplies the missing plugin for 71 KB downloaded. Its
one new dependency of substance, libwebp, is already on every machine
by way of libvips.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store theme backgrounds as webp

WebP codes both of the things these backgrounds are made of better than
the formats they were in: the photographs, where its lossy mode is worth
a third or more over JPEG at matched quality, and the flat art and dot
patterns, where its lossless mode undercuts an oxipng-packed PNG.

28 of them become lossless webp and decode bit-for-bit identically
(AE=0), so the dot patterns and flat-shaded pieces carry no quality
question at all. That includes 0-launch, whose alpha channel comes
through intact. The other 51 are photographs held to the same 38 dB
PSNR floor as the JPEG pass, landing between 38.0 and 54.6 dB. Every
image keeps its exact pixel dimensions, for 29.8 MB.

Each one is encoded from the original as it stands in quattro rather
than from the file the earlier commits produced, so nothing picks up a
second generation of loss on the way here.

13 stay JPEG. WebP is plainly larger for most of them, and three are
grainy enough that its filter smooths the grain instead of coding it:
PSNR plateaus near 34 dB however high the quality goes, well under the
floor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:21:30 +02:00
83987718cf Give the Quake console its own file, an agent, and half a screen (#7420)
* Open the scratchpad with the default agent already in it

on_created_empty fires when the special workspace is created empty, so the
agent starts the first time the console drops down instead of at boot, and
comes back on the next open if you close it.

The exec rule pins the workspace rather than trusting the spawn to inherit
it: Hyprland only tags a process with its origin workspace while
misc.initial_workspace_tracking is on, and we turn that off.

Nothing to guard for a missing default agent. Omarchy picks none for you,
and omarchy-agent exits without opening a window when none is set, so the
scratchpad just opens empty until one is chosen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Move the console into its own file and size it to half the screen

The scratchpad's presentation was scattered through looknfeel: a dim in the
decoration block, a workspace rule below it, two animation leaves further
down again. Gathered into qconsole.lua, where the whole console is one
readable thing.

Sized to half the screen while it moved. A window rule cannot do that: its
size expressions resolve once, when the window maps, so rescaling the
monitor afterwards leaves a console that is no longer half of anything.
Gaps are re-applied by the layout, so the console is sized by the gap left
underneath it, recomputed from the monitor whenever the layout changes.

Monitor dimensions come back in physical pixels while gaps are logical, so
the scale comes out before the reserved area comes off. That arithmetic is
the whole trick, and the test pins it at 1x, 2x and 1.5x.

The test runs lua with an explicit "-". Bare `lua <<EOF` reads stdin as a
REPL and exits 0 even after an error, which would leave its assertions
unable to fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Only rewrite the console rule when its size actually changes

Refitting replaces the rule in place rather than stacking a new one, so
there was no leak, but each write still schedules a monitor and window state
refresh and monitor.focused fires on every hop between screens. Remember
what was last written and skip the write when the number has not moved.

Also say out loud that the scale guard is what keeps the arithmetic below it
safe: a monitor handle that has outlived its output answers nil to every
field, and a layout change is exactly when that happens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop the active window border inside the console

The gradient border marks which window has focus, which the console does not
need: it is only ever focused while it is open, and the dimmed workspace
behind it already sets it apart. On a single agent terminal the highlight
just reads as a frame around the panel.

no_border on the workspace rule pins the border to 0 at workspace-rule
priority, so it applies to whatever ends up in there without touching the
global border.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Omabot <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 22:24:56 +02:00
ec779715bd Shrink theme backgrounds without touching resolution (#7266)
* Re-encode over-encoded theme backgrounds

Several backgrounds shipped at quality 95-100, which buys nothing visible
on a wallpaper but costs 20 MB. Re-encode those at quality 85, leaving
resolution untouched -- every file keeps its original pixel dimensions.

Only files stored above quality 90 are touched, and only when the result
saves at least 15% and stays above 38 dB PSNR against the original. The
34 backgrounds already stored at quality 85 or below are left alone
rather than pushed through another lossy generation for a few hundred KB.
osaka-jade/2-shaded-entrance is skipped for that reason: nothing clears
both bars. ristretto/2-coffee-beans is re-encoded at 90 instead of 85,
where 85 fell below the PSNR floor.

Theme backgrounds drop from 107 MB to 87 MB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store nord's night-hawks background as a palette PNG

The file uses only 8 distinct colors but was saved as truecolor RGB,
costing 1.8 MB for an image an indexed palette stores in half the space.
Converting to a palette PNG is lossless: same dimensions, and zero pixels
differ from the original.

1782 KB -> 909 KB.

The other palette-eligible PNGs are already indexed, and re-compressing
them with ImageMagick only makes them bigger, so they are left alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Recompress theme PNGs losslessly with oxipng

Every background PNG was left at its authoring tool's default deflate
settings. Running oxipng over them re-packs the same pixels: all 31 files
decode bit-for-bit identically (AE=0) at unchanged resolution, for 981 KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store photographic backgrounds as JPEG instead of PNG

Four backgrounds were shipped as PNG despite being photographs and
painterly illustrations, where lossless coding buys nothing the eye can
see. Re-encoding them at the same q85 used by the other backgrounds keeps
every pixel dimension and stays above 40 dB PSNR, for 3.3 MB.

The remaining PNGs stay PNG: JPEG is larger for the dot patterns and the
flat-shaded pieces, and 0-launch.png genuinely uses its alpha channel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 18:29:23 +02:00
OmarchybotandGitHub 262d6f0681 Switch to the mise-bin package (#7244)
* Switch to the mise-bin package

mise-bin carries mise's own release artifacts from the Omarchy repo --
PGO+BOLT-optimized on x86_64, glibc-native on both arches -- instead of
Arch's mise, and tracks jdx/mise releases directly.

Existing installs need a migration because the two packages conflict, and
omarchy-pkg-add cannot make the swap: pacman answers its own conflict
question with No under --noconfirm and fails the transaction. --ask=4
answers that one question, so mise-bin replaces mise in a single
transaction -- which is also what keeps omarchy-zsh and omarchy-fish, both
of which depend on mise, satisfied through the swap by its provides.

* Guard the swap with a conditional instead of an early exit

Two-path control flow takes an if, per the style guide; the early exit only made the swap line unreachable from a distance.
2026-08-17 05:11:56 -04:00
OmarchybotandGitHub 7488eaded4 Document remapping the CapsLock compose key (#7091)
* Document remapping the CapsLock compose key

* Clarify restoring CapsLock behavior
2026-08-16 05:47:41 -04:00
33cda8b602 Add --gh-keys so sshd setup can run without prompts (#7086)
* Add --gh-keys so sshd setup can run without prompts

Grabbing keys from GitHub was reachable only through the interactive menu: pick
"Grab key from GitHub", then type the username into a second prompt. So the one
path that needs no secret pasted around was also the one path a script could not
take, and setting a machine up over ssh or from a provisioning run meant falling
back to --key with a key copied by hand.

--gh-keys <username> takes the same path the prompt did. The fetch and authorize
logic is unchanged and now shared, with the prompt reduced to asking for the
username and handing it over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Reject a missing --gh-keys username before setting anything up

The username was only checked for being absent entirely, and only after the
server was installed and the firewall opened. So `--gh-keys=` with an unset
variable behind it configured the machine and then dropped into the interactive
menu, and `--gh-keys --help` took --help as the username and set the server up
on its way to failing the fetch -- a help flag that changes the system.

Check the value where it is parsed, and reject one that is empty or shaped like
an option.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 10:14:42 +02:00