Author SHA1 Message Date
ZacharyZhang-NYandClaude Fable 5 e51542d347 Point the site and README at the 4.0.0.alpha-cn.8 release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:55:38 -04:00
ZacharyZhang-NYandClaude Fable 5 640c0d4291 Begin the 4.0.0.alpha-cn.8 release: cn/release to 8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:37:31 -04:00
ZacharyZhang-NYandClaude Fable 5 ed9ef8bc04 Propagate key-trust failures and let doctor --fix heal a keyless mirror line
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:35:24 -04:00
ZacharyZhang-NYandClaude Fable 5 653656e439 Trust the registry key inside the mirror fix so Required never strands a client
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:31:13 -04:00
ZacharyZhang-NY 8715c20af2 Merge remote-tracking branch 'origin/quattro' into quattro
# Conflicts:
#	default/pacman/pacman-stable.conf
2026-08-28 23:18:35 -04:00
ZacharyZhang-NYandClaude Fable 5 f9c4085982 Make CJK lang rules survive 50-omarchy.conf's generic-family assign (fixes #4)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:12:03 -04:00
ZacharyZhang-NYandClaude Fable 5 074247c79a Restore the app catalog verification date to its actual audit day
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:09:26 -04:00
ZacharyZhang-NYandClaude Fable 5 dc0513a519 Remove WeCom from the China app catalog
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 23:05:22 -04:00
ZacharyZhang-NYandClaude Fable 5 495b24cedf Point the site and README at the 4.0.0.alpha-cn.7 release
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kb7vDj6PHwymeDeSUUk1eX
2026-08-28 22:23:26 -04:00
Ryan HughesandGitHub f0672772d2 Merge pull request #8268 from Skeptomenos/fix/copy-url-python-shim-recursion
Avoid mise shim recursion in Copy URL migration test
2026-08-28 21:54:19 -04:00
Ryan HughesandGitHub 62eb5182d0 Merge pull request #8835 from basecamp/fix-browser-policy-exit-trap
Fix migration 1787515927 failing on Bash 5.3
2026-08-28 21:24:32 -04:00
Ryan Hughes 6b10dbf191 Harden Firefox policy dirs even when the theme refresh fails 2026-08-28 19:00:03 -04:00
Ryan Hughes 5925929cb6 Stop the browser policy EXIT trap from reporting a clean run as failed 2026-08-28 19:00:03 -04:00
Ryan HughesandGitHub 7d58bb9a62 Merge pull request #7972 from acrogenesis/harden-browser-policy-dirs
Stop world-writable browser policy directories
2026-08-28 18:00:20 -04:00
David Heinemeier HanssonandGitHub 6dd9aa55cc Merge pull request #6919 from AksharP5/agent/resolve-dialog-pointer-focus
Stop DaVinci Resolve dialogs from recapturing pointer focus
2026-08-28 23:41:00 +02:00
Ryan HughesandGitHub 468b511249 Merge pull request #8397 from ErikMelton/unauthorized-http-get-requests-from-notifications
Require textFormat declaration for all Text elements
2026-08-28 15:50:19 -04:00
David Heinemeier HanssonandGitHub 06e32d243d Merge pull request #8198 from bastidotnet/harden-apple-brightness-device-cache
Validate the cached Apple-display device path before use
2026-08-28 14:42:22 +02:00
d1845245d3 Unquote the new variables inside [[ ]]
AGENTS.md asks for unquoted variables inside `[[ ]]`, with quotes reserved for string literals being compared. The three conditions added here quoted them.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 21:40:31 +02:00
9d8c0176d1 Make the cache tests fail when either check is removed
Neither half of the validation was covered. Dropping `&& -c $cached` from the wrapper left the whole file green: all three poison values fail on the pathname prefix, so none of them ever reached the character-device test. A path that matches the hiddev glob but is not a device now covers it, and it is the real case rather than a synthetic one -- the display replugs, the interface renumbers, and the cached node is gone. It is added only when the host has no such node, so a machine with the display attached cannot fail there spuriously.

The no-XDG_RUNTIME_DIR assertion had the same problem for the opposite reason: its decoy held a path the validation rejects on its own, so restoring the `${XDG_RUNTIME_DIR:-/tmp}` fallback left it passing. It asserts on the open now instead of on the contents -- a FIFO with no writer blocks whoever opens it, so a wrapper that consults the path hangs and one that ignores it exits. mkfifo is atomic and fails outright if the path is taken, so it still neither overwrites a file nor follows a symlink at the fixed path.

Clearing created_tmp_cache as soon as the decoy is removed keeps this run's EXIT trap from deleting a concurrent run's decoy at the same fixed path, which would have let that run pass against the old code.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 21:40:31 +02:00
OmarchybotandGitHub 83881e979b Merge pull request #7984 from Chessing234/fix/webapp-name-slashes
Keep a web app name out of the launcher's directory structure
2026-08-27 19:40:46 +02:00
9ece53cede Prove the web app name guard, and reject before the icon is fetched
The slash guard was the only thing keeping a name out of the directory structure, and nothing tested it: deleting it left the suite green, because creating the launcher directly in the applications directory already makes the redirect fail on its own, with a raw bash error instead of the message. The assertion is on the message now, alongside the traversal case the guard actually closes -- on quattro a name of `../../../../escaped` writes its launcher clean outside the applications directory.

The interactive prompt read the name, fetched the favicon, wrote it and updated the icon cache before the name was ever checked, so a URL typed into the Name field left an icon behind on every attempt. Validating as soon as the name is read covers both paths from one place.

Removing by name also scanned unconditionally, so a machine with no applications directory printed a find error where omarchy-remove-gaming-xbox-cloud does not hide stderr.

🤖 Generated by Opus 5 in Claude Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 19:25:25 +02:00
David Heinemeier HanssonandGitHub 946704f309 Merge pull request #8224 from kevinmcconnell/once-requires-sudo
Run ONCE with sudo when installing
2026-08-27 17:36:31 +02:00
Ryan HughesandGitHub c5a5e14e99 Merge pull request #8072 from basecamp/require-omarchy-repo-signatures
Require signed packages from the Omarchy repository
2026-08-27 11:29:12 -04:00
4be440b501 Close six ways the textFormat scan reported success without checking
Each of these is a Text rendering external data with no textFormat, written in
a form that passed silently. None exists in this tree, so they were holes in
the guard rather than live exposures — but a guard is only worth what it
catches, and every one of them is a single line someone could plausibly write.

  Text /* why */ {          strip_noise knew // and not /* */, so a block
                            comment between the type name and its brace hid
                            the element from every rule at once
  QQ.Text { ... }           a namespaced import made the name compare unequal
                            to `Text`, and the element was skipped outright
  visible: textFormatEnabled  textFormat was matched as a substring, so a
                            lookalike property exempted the whole block
  component Info:           a component root with its Text on the next line;
    Text {                  the one-line form was covered and this was not
  an unreadable subdirectory  rglob() swallows a directory it cannot enter, so
                            a locked subtree scanned as though it were empty

The scan moves out of the heredoc into qml-text-format-scan.py, taking its root
as an argument, because nothing could run it over anything but the real tree —
and a scanner whose only input always passes cannot be shown to fail. The test
now runs it over nineteen fixtures, one per form above and one per form the
scan already handled, so a later edit that loosens it fails here instead of
going unnoticed until something renders a remote image.

Two limits stay open and are written down in the module docstring rather than
papered over: text assigned from elsewhere (a Binding element, PropertyChanges,
an onCompleted assignment, a property alias onto a child) is invisible to a
scanner that reads each element's own declaration, and a regex literal holding
a brace throws off the brace depth. Neither shape exists in this tree and both
need a QML parser, not another regex.

Co-Authored-By: Codex XHigh <noreply@openai.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 17:28:13 +02:00
David Heinemeier Hansson 7026ede90b Strip image tags after the newline rewrite, not before it
The card binds the body Text to styledBody, which rewrites newlines to <br/>
*after* sanitizeBody has run. That rewrite inserts tag syntax into text the
stripper deliberately kept: a kept tag may hold a `<` of its own, and `<x`,
newline, `<img src="http://host/x.png">` is one tag named `x` to both the
stripper and Qt, so it survives whole — until the rewrite splits it into
`<x<br/>` and a live image tag the input never contained.

Measured against Qt 6.11.2 with an offscreen StyledText and a local HTTP
server: that body issues the GET after this branch's sanitizer and issues
nothing before it, because the one-pass /<img[^>]*>/gi it replaces deleted the
inner substring outright. The whole-tag bound is still the right trade — it is
what stops the stripper manufacturing tags — but it only holds if nothing edits
the string afterwards.

So move the rewrite into NotificationLogic, next to the reasoning it depends
on, and strip again after it. What Qt parses is then what was checked last. The
tests assert on styledBody for the same reason, since sanitizeBody's output is
no longer the string that reaches the renderer, and a regex assertion pins the
card's binding because no JavaScript assertion can see a QML property.
2026-08-27 16:53:55 +02:00
David Heinemeier HanssonandGitHub 9d02bb08f8 Merge pull request #8549 from basecamp/crash-notification-mute
Let a crash diagnosis mute that program's notifications
2026-08-27 13:19:46 +02:00
OmarchybotandDavid Heinemeier Hansson b68d4142d7 Cut the crash-mute section of the skill to what it instructs
The section had grown a paragraph per review round, each one explaining why the last was right, until one offer took a third of the file. Most of it was reassurance about what the command refuses rather than anything an agent has to do, and the command enforces that itself whatever the prose says.

What is left is the instruction: offer it and never run it unprompted, say how to lift it, which of the two names to pass and why the binary is the better one, quote it because the name is the crashed program's to choose, and name the interpreter collision before muting python or node on someone's behalf. Fifty-four lines to thirty-two, with nothing dropped that changes what the agent does.
2026-08-27 12:28:44 +02:00
ea6ee9440a Add omarchy-crash-mute to mute and unmute one program
The mute was reachable only as `omarchy-toggle crash-ignore/<program>`, which asks whoever runs it to know the flag layout, to reduce a binary's path to the name the watcher keys on, and to have read the rule that a name climbing out of that directory writes an unrelated toggle. All of that was carried in the skill's prose, which is the wrong place for a rule that has to hold: prose is advice, and the thing being advised about is a name the crashed program chose.

So it is a command now. `omarchy crash mute hyprland` silences that program, `off` lifts it, `toggle` flips it, and no argument lists what is muted. It takes the binary's path as readily as the name and reduces it the way the watcher does, so the `Executable:` line from `coredumpctl` can be handed straight to it; it refuses what is not one component of a name, so it cannot be talked into writing outside its own directory whatever it is given; and it re-reads the flag afterwards and reports what is now true rather than what was asked for. The listing counts only regular files, because that is all the watcher honours -- anything else in there would read as muted while the crashes kept arriving. A leading `--` is consumed so a program named `-h`, which the router would otherwise answer with its own help, can still be muted.

The watcher gained an unrelated fix that this uncovered. Its fields are read with `IFS=$'\t'`, and tab is IFS whitespace, so an empty field collapsed into the next delimiter and shifted every field after it along one: a crash whose comm was empty had a path read as its pid and was discarded as somebody else's. A process can set its comm to nothing, so that was reachable. Empty fields now arrive as a dash like missing ones, and a dash joins the empty and dot cases that fall back to `unknown`.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 11:26:28 +02:00
OmarchybotandDavid Heinemeier Hansson eeb4206c7b Say in the manual what the skill already says about quoting
The manual had single quotes covering "punctuation your shell would otherwise read as its own", which is more than they do: a name containing a single quote closes them, and the rest of it is read as shell. The skill states that correctly and the manual did not, so the one document a person reads before typing the command was the one making the claim that does not hold.
2026-08-27 10:38:18 +02:00
8d14869689 Let a crash diagnosis mute that program's notifications
A crash that is understood is not a crash that stops: an upstream bug waiting on a release, a program that dumps core every time it exits. The diagnosis explains it once and the toast keeps arriving, and the only answer Omarchy had was Crash Capture, which turns off every program's notifications in order to silence one.

The watcher already resolves a name to dedupe on and announces that same name in the toast, so the mute is keyed on it: a flag file under toggles/crash-ignore/, written by the existing omarchy-toggle and read by the existing omarchy-toggle-enabled. One flag per name rather than one list, so `on` mutes, `off` un-mutes, and `ls -A` shows what is muted, with no new file format and nothing to parse. It is the executable's basename wherever one was recorded, falling back to the process name, which the kernel truncates to fifteen characters -- muting the truncated form would match nothing, forever, while looking like it worked.

The name is not always a name, though, and the mute turns it into a path. A program picks its own comm and prctl takes anything, including slashes, and the watcher falls back to comm whenever a crash carries no absolute executable. So it is stripped to its last component first: without that, `a/../bar-off` is a legal comm aimed at an unrelated Omarchy flag, letting a crashing program suppress its own notification and letting a user who accepted the offered mute hide their bar instead. Stripping does not always leave a component either -- `/` leaves an empty string, which is no kind of array subscript and no kind of toast, and `.` or `..` names a directory that omarchy-toggle would touch and report success on, leaving a mute that never matches. Both fall back to `unknown`, the word omarchy-agent-crash already uses for a name it does not have, and which mutes like any other.

The skill offers this at the end of a diagnosis and never runs it unprompted, which makes it the single change a diagnosis may make to a system it otherwise only reads. It tells the agent to use the name it was handed rather than re-derive one, since the watcher resolved that name already and the two agree for ordinary names and not for strange ones; a diagnosis started by hand from `omarchy agent crash <pid>` is given no name and gets the derivation instead. It also says to treat the name as hostile text rather than as a word -- it is whatever the crashed program's author called a file, so a single quote inside one closes the quotes around it and the rest runs as the shell -- and to check the flag arrived rather than assume it.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 10:32:10 +02:00
Luke ParkeandGitHub eb7ecd13f3 Keep Ori interactive when launched with a prompt (#8455) 2026-08-27 09:21:07 +02:00
0260d2accb Stop the textFormat test from passing when it has not checked
The root rule matched only a file-level root Text, of which this tree has
exactly one. QML inline components are roots for the same reason — the
`text` of `component InfoValue: Text {` comes from every caller, so the
file it lives in never binds it — but they sit inside another element, so
the depth-1 test never saw them. Six went uncovered while the test
reported green, among them the network panel's InfoValue, which callers
bind to the IP address and gateway.

Six more ways to write a Text were read as clean rather than as unreadable:
an opening brace that is not last on its line, a brace on the line after
`Text`, a one-line block containing nested braces, a wrapped binding split
by a comment or a blank line before its `+` (which exempted a dynamic
binding as a literal), and a root Text indented from column zero. Require
the forms a line scanner can read instead of parsing QML; the tree already
writes every Text that way.

Last, a run that read no files reported success. A checkout with no shell/
QML now fails instead, since an all-clear from a scan that opened nothing
is the one answer this test must never give.

Each case is covered by a fixture that fails without its fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: OpenAI Codex (gpt-5, xhigh) <noreply@openai.com>
2026-08-26 17:42:07 +02:00
David Heinemeier HanssonandClaude Opus 5 e428dc2627 Strip image tags whose separator Qt skips but \s does not
QQuickStyledText skips the characters between `<` and the tag name with
QChar::isSpace(), which counts U+0085 NEL. JavaScript's `\s` does not, so
isImageTag() read no name at all from a tag written as `<`, U+0085, `img`,
kept it, and Qt then read `img` and issued the GET the stripper exists to
prevent. Measured against Qt 6.11.2 with an offscreen StyledText and a
local HTTP server.

Read the name by skipping everything that is not part of it rather than by
matching the separator, so the two definitions cannot drift apart again.
Over-skipping is the safe direction: it can only classify more runs as
images, and dropping a run never manufactures a tag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 17:10:17 +02:00
Erik Melton 6e962b4466 Address review comments: Enforce stricter tag handling and image sanitation in notifications. 2026-08-26 16:36:12 +02:00
Erik Melton 3af7675a10 Require textFormat declaration for all Text elements. 2026-08-26 16:14:51 +02:00
0ae1694830 Constrain the tzupdate sudoers rule to a single timezone argument (#8194)
The wildcard granted passwordless root for timedatectl set-timezone plus any trailing arguments, so -H/--host and -M/--machine reached the SSH and machine transports as root. Systemd 261 guards argv injection into ssh, but -H still drives root's SSH client at an attacker-chosen host, and the transport resolves its helper through PATH; only Defaults secure_path stands between that and a planted ssh running as root. Match the argument with an anchored POSIX ERE that admits exactly one timezone token (no whitespace, no leading-dash segment, no traversal component), so no second argument and no option can ever match. The sole caller, omarchy-menu-timezone, passes one list-timezones value and is unaffected.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 22:30:09 +02:00
77305ed3b9 Enable Dell XPS 13 sidecar speaker amplifiers (#7032)
The Dell XPS 13 DX13260 drives its two CS35L56 sidecar speaker amplifiers through a quirk that Linux only gains in 7.2, so until Arch ships that kernel the machine plays through one amplifier with no bass. The dell-xps13-sidecar-amps package selects the same driver path with a module override; this installs it on that exact machine and nowhere else.

The detector requires both the DX13260 product name and SKU 0E53, because the override forces a quirk value rather than merging into one, and a machine that gets it wrong loses whatever quirk the kernel would have chosen for itself.

Pacman registers a package even when its post_install scriptlet fails, so the leaf calls dell-xps13-sidecar-amps-apply itself instead of trusting the install to have applied: a failed cleanup or boot-image rebuild has to reach the caller rather than hide behind a package pacman considers installed. That is also why the migration marks reboot-required only after the apply succeeds — a migration that exits non-zero keeps no completion marker and retries the apply on the next run, even though pacman already has the package.

The leaf runs after intel/ptl-kernel.sh rather than beside the other Dell leaf at the top of install/hardware/all.sh, so its boot-image rebuild sees the Panther Lake kernel that step swaps in rather than the stock one it removes.

Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-25 22:29:27 +02:00
David Helmus 45749c5b68 test: cover Python shim bypass
Place a synthetic python3 shim first in PATH so CI catches any regression that resolves REAL_PYTHON through user-managed shims.
2026-08-25 21:09:45 +02:00
acrogenesis bafc9a1000 Write browser theme colour through a passwordless helper
Managed policy dirs are enterprise trust roots, so they stay 0755 root:root. The menu path takes root for that one write through a sudoers glob of six hex digits, the same shape as omarchy-dns, and falls back to pkexec where the grant is not installed. Drop omarchy-browser-policy; a group member could plant any JSON, not just a colour.
2026-08-25 13:01:01 -06:00
acrogenesis 44a186afe4 Replace planted policy directory symlinks instead of following them
install -d follows a managed or distribution symlink and would chmod the target. Unlink those paths first, and treat a dangling symlink as a directory the migration still has to repair.
2026-08-25 12:14:52 -06:00
acrogenesis bebe19bc70 Harden browser policy parent directories and validate theme RGB
install -d follows a planted ancestor symlink, and a writable parent can rename the managed leaf aside. chromium.theme is user-installed, so only a 0-255 RGB triple becomes a colour.
2026-08-25 12:10:08 -06:00
acrogenesis 87dfa14c56 Keep a trusted Firefox policies.json when repairing the directory
A world-writable distribution dir failed the hardened check even when
policies.json was already root-owned, and setup then overwrote it.
2026-08-25 12:04:03 -06:00
acrogenesis b0e6611c70 Require root-owned Firefox policies before skipping repair
The hardened gate only looked at the distribution directory. A regular
policies.json planted under the old 777 mode would then be left in place
if the directory later looked 755/root.
2026-08-25 12:04:02 -06:00
acrogenesis 95b791af16 Stop world-writable browser policy directories
Chromium managed policy is mandatory for every profile. World-writable
dirs let any local uid plant policy, including force-installed
extensions. Write goes through the omarchy-browser-policy group at 2775
so theme colour still works without other-write.
2026-08-25 12:04:02 -06:00
Kevin McConnell 2c93e66b0c Run ONCE with sudo when installing
The install user is no longer in the docker group by default, so a bare
`once` cannot reach the Docker socket. Run with `sudo` instead.

The script already requires sudo to install the command and enable the
service, so we can safely use it for the initial command launch as well.
2026-08-25 16:12:10 +01:00
Naeem MalikandGitHub 9301092404 Update Omarchy on Mac guide link to omarchy-mac repo (#8192) 2026-08-25 17:04:13 +02:00
4cd8a081cb Fix Codex usage collection on 0.149 (#7649)
* Fix Codex usage collector approval policy

* Capture codex argv with boundaries in the scanner test

The stub joined its arguments with "$*", so the assertion compared one
flattened string and could not tell five arguments from fewer containing
spaces. Passing "-s read-only" and "-a on-request" as single arguments --
which codex rejects as an unexpected argument -- passed the test. NUL
separation and an array comparison keep the boundaries the assertion is
about.

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

---------

Co-authored-by: Omabot <omabot@omarchy.org>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 16:07:17 +02:00
bastidotnet e53548fae2 Harden the test's temp-file handling against a symlink race
The /tmp-fallback case did check-then-create on a fixed /tmp name, a
TOCTOU/symlink race, and the EXIT trap only cleaned $TMPDIR. Create the decoy
atomically with noclobber (O_EXCL) so it refuses to overwrite an existing file
or follow a symlink at that path, and remove it on exit only when this test
created it. The fixed path is required (it is exactly the path the old code
would form), so a random mktemp name cannot replace it. Addresses the Copilot
review on #8198; the wrapper fix is unchanged.
2026-08-25 13:51:39 +02:00
bastidotnet fe56d68e90 Validate the cached Apple-display device path before use
The cached device path was trusted for merely existing, not for being a hiddev node, and fell back to a predictable /tmp path when XDG_RUNTIME_DIR was unset. Validate the cache shape (hiddev char device) and cache only under the user-private runtime dir; asdcontrol already gates non-Apple devices downstream, so this is defense-in-depth in the layer Omarchy owns.
2026-08-25 13:24:00 +02:00
23dab9ec4d [Security] Stop the FIDO2 setup staging its authfile at a predictable /tmp path (#7904)
* [Security] Stop the FIDO2 setup staging its authfile at a predictable /tmp path

pamu2fcfg wrote to /tmp/fido2 and the registration was then moved into place
with `sudo mv`. Any other local user can pre-create /tmp/fido2, and rename(2)
does not dereference the final component, so the privileged move installed the
attacker's symlink itself as pam_u2f's global authfile -- a file consulted by
`sufficient` lines in /etc/pam.d/sudo and /etc/pam.d/polkit-1.

The same move also carried the staged file's ownership into /etc, so on every
install to date /etc/fido2/fido2 is owned by the invoking user at mode 0644.
That needs no attacker: anything running as that uid can add its own credential
and satisfy the machine's sudo prompt without root.

Stage under mktemp and hand the bytes to `install` instead, so the authfile is
always a fresh root-owned regular file rather than an inode a non-root user
still controls. Guard the already-registered check with -L, which -f would
otherwise follow, and reject a symlinked /etc/fido2 in the remove path for the
same reason. A migration takes ownership of authfiles left behind by the old
code; it reports a symlink rather than repairing one, since chown would follow
it and removing it would strip sudo from anyone whose only credential is the
token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012e6WagC5iUigCGoK1tQeFz

* Install the FIDO2 authfile with -T and refuse a non-regular path

`install SRC DEST` without -T treats an existing directory at DEST as a
destination directory: it drops the credential inside as
/etc/fido2/fido2/tmp.XXXX, exits 0, and setup reports a successful registration
while PAM goes on reading a path that is not a file. -T makes that an error.

The already-registered check has the same blind spot from the other side. -f
follows symlinks, so it reads a symlinked authfile as a registration and leaves
it in place, and is false for a directory, so it tries to register over one.
Only a regular file is a pam_u2f authfile; anything else is now refused with the
same advice to remove it and set FIDO2 up again.

The test deleted every staged path that fell outside its scratch directory,
taking the path from the script under test and already resolved through any
symlink -- so a script staging through one would have named a file of the user's
and had it unlinked. It now unlinks only a file its own stub wrote into.

On a machine that already has /etc/fido2/fido2 the staging assertions cannot run
at all, and the file used to pass without exercising one of them. That branch now
asserts what the host state promises instead: a regular authfile still has to be
recognised as a registration and left alone, and anything else has to be refused.

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

* Replace the FIDO2 authfile inode rather than chowning it in place

Permission is checked at open(2), not at write(2), so a descriptor the
registering user opened on the authfile while it was still theirs stays writable
through chown and chmod alike. pam_u2f resolves /etc/fido2/fido2 to that same
inode, so the repair left the account it authenticates able to append a
credential it controls -- the exact state the migration exists to end, now
recorded as migrated and never revisited.

Installing a fresh root-owned copy and renaming it over the path leaves any such
descriptor writing to a file nothing reads. Credit to #7703, which reached the
same conclusion independently. An interrupted run heals: the staged copy is
root-owned 600 and inert, no marker is written, and the next run replaces it.

A directory or device at the authfile path is no more ours to rewrite than a
symlink is, and chmod 600 on a directory would only make it untraversable, so
both are now reported rather than repaired.

The repair had no test, because it names an absolute path no unprivileged suite
can write. It is exercised through a scratch copy with that one literal
retargeted, rather than by reading the path from the environment: the migration
hands `install` and `mv` root, and an operand the caller can choose is a
privileged write to anywhere. The copy is only as honest as the substitution, so
the test fails if the migration stops naming the path exactly once.

Covered: the no-op on a machine that never registered a key, which must not cost
a password prompt; the repair itself; the new inode; the absence of a staged copy
afterwards; a second account finding it done; and the symlink and non-regular
cases. Each assertion was checked against a mutation that defeats it -- notably a
repair with the right install call, mode, content and cleanup that writes through
the old inode, which only the inode assertion catches.

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

* Finish hardening FIDO2 authfile installation

* Guard the FIDO2 directory and the stage path the setup writes through

install -d follows a symlink at /etc/fido2 and applies the mode and ownership to whatever it points at, so the credential would be staged and published inside the link target and that directory silently reopened to root:root 755. The leaf guard above it only covered fido2 itself, and this is the same threat omarchy-remove-security-fido2 already names on its side.

mktemp's output is an operand for a privileged tee, chmod, mv and rm. The migration validates it before any of them run; the setup did not, so take only the name it asked for there too.

The suite was guarded on the host's own /etc/fido2/fido2 and exited early when one existed, which meant the staging assertions asserted nothing on exactly the machines that use FIDO2. Drive a retargeted copy the way the migration suite already does, so every branch is a fixture and all of them run everywhere.

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

* Stop the FIDO2 migration recording a repair it never made

omarchy-migrate writes the per-user completion marker on any zero exit, so the two states this migration cannot repair got one line in the update terminal and were then silenced for good: no login notice, no re-run, the migration recorded as done having repaired nothing. Those are precisely the machines where the authfile may already be under someone else's control, so raise them through omarchy-notification-send as well, where they outlive the scrollback. Delivery is best-effort: a machine with no user bus or no notification server must not abort the migration and take every later one with it.

The early exit had the same shape of problem. It read the authfile unprivileged, and the old setup created /etc/fido2 with `sudo mkdir -p`, which took the union of the caller's umask and sudoers' 0022 — so registering under `umask 077` left the directory mode 0700 with the user-owned authfile still inside. Absence and "cannot look" are the same answer to those tests, and the migration exited 0 and marked itself complete. Ask root whether a registration is actually behind an untraversable directory before reopening it, so an aborted setup that left an empty directory, or one an administrator keeps private, does not have its mode widened and its group and special bits discarded for a repair it does not need. A machine that never set FIDO2 up has no directory here and still reaches exit 0 without a password prompt.

The notification assertion checks argument shape rather than a substring of the command line. The glyph is a private-use codepoint, and losing it shifts every argument left: -g swallows the headline, the body becomes the title, and the message goes out with no description — which a substring match reads as a pass.

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

* Cover the FIDO2 removal's symlink guard

The -d to -e || -L change is load-bearing for the threat its own comment names — a dangling link at /etc/fido2 that -d reads as absent, left for the next setup to install an authfile through — and it was the one part of this work with no test behind it. Name the directory once so the suite can retarget a copy, the same seam the setup and migration suites use, and assert both halves: the link goes, and the directory it pointed at does not.

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: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 12:09:17 +02:00
9285b19d6a [Security] Stop USB device names from being executed as Hyprland Lua (#8129)
* Stop device names from being executed as Hyprland Lua

Hyprland input-device and monitor names come from USB descriptors and
hyprctl output, so they are attacker-influenceable, yet the toggle and
monitor commands interpolated them straight into hyprctl eval and into
generated Lua that Hyprland re-executes on every reload. The input-device
toggle keys are bound with locked = true, so a malicious USB name reached
Lua code execution from the lock screen; a persisted disable made it run
on every start. This closes that class everywhere it appeared.

- The touchpad/touchscreen disable is now the device name in a plain-text
  sidecar file, read back by a packaged Lua module on reload, never a
  generated Lua file. hyprctl eval Lua-quotes the name and control
  characters are rejected outright.
- Dropped the shipped *-disabled.lua templates so nothing seeds a
  disabled state to /etc/skel, making the name file the single source of
  truth read from a hardcoded ~/.local/state to match the sibling tools.
- The reload loader excludes those two legacy filenames, so a leftover
  generated *-disabled.lua on a not-yet-migrated install can never be
  sourced as code again; a migration then recovers the device name from
  it and deletes it, sanitizing installs that ran the vulnerable version.
- All four monitor scripts (internal, mirror, clamshell, scaling) now
  validate an output name against a plain-connector-name pattern before
  writing it as Lua, closing the same latent pattern in the siblings.
- paths.lua treats a set-but-empty XDG_STATE_HOME as unset, matching the
  bash side so state is never read from the filesystem root.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144ZDt44vtxjyF8j9Y88NrM

* Let a failing Lua assertion fail the test

lua discards the status of a chunk read from stdin, so a blown assert printed its traceback and still exited 0: the surrounding `set -euo pipefail` never fired and the following `pass` printed `ok`. Every Lua block in these two files was unenforced, including the assertion that a quoted `hyprctl eval` cannot reach `os.execute` and the negative control that proves the test can detect the injection at all. Passing the chunk as a script argument makes lua report the failure.

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

* Re-apply a recovered input-device disable to the running session

The package hook reloads Hyprland during `omarchy-update-system-pkgs`, before `omarchy-migrate` runs, and at that reload the generated Lua is already excluded while the name file does not exist yet — so a touchpad or touchscreen the user had switched off comes back on, and stays on until their next login. Reload once more once the name has been recovered, which is the same path a login already takes to read it.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Omarchybot <omabot@omarchy.org>
Co-authored-by: Codex XHigh <codex@openai.com>
2026-08-25 11:03:12 +02:00
4637735aa2 Pin trusted PATH in privileged DNS helper (#8172)
* Pin PATH to trusted dirs when omarchy-dns holds root

A dev link prepends a user-writable checkout bin/ to sudo's secure_path,
so the passwordless `omarchy-dns Cloudflare` sudoers rule lets root
resolve a bare helper (dirname, install, tee, nmcli, ...) out of that
checkout — turning checkout-write access into arbitrary root execution.
Pin PATH to trusted system directories once EUID is 0, leaving the
unprivileged wrapper phase free to locate sudo/pkexec on the caller's
PATH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YUWoHbBoKMjsjV6X3nu1H5

* Assert the trusted-PATH pin is gated on root, not merely present

The EUID assertion matched `(( EUID == 0 ))` anywhere in the file, and require_root has carried that exact test since long before the pin existed. Deleting the pin left the assertion passing, so it stood for nothing: a run with the pin neutered reached the behavioural probe with both greps green. Anchor on the unindented guard and require the pin to be the line it opens, which no other construct in the script satisfies.

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

* Skip the DNS trusted-PATH probe where user namespaces are unavailable

`fail` ends the file, so a sandbox or hardened kernel that refuses unprivileged user namespaces did not just lose the probe — it took the two elevation assertions below it down as well, reporting a product defect where there was only a missing capability. The non-graphical suites are meant to run on any machine and treat a skip as a passing test, the way require_compositor and plugin-add-test.sh already do. Gate the probe on the namespace it needs and say so when it is absent; the static checks above and the elevation checks below run either way.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 09:37:55 +02:00
68ab12f77d Share the git URL check, and refuse the transports Omarchy does not clone from (#8174)
* Share the git URL check between theme-install and plugin-add

Both commands clone a URL a stranger can choose, and each carried its own copy of the rule that refuses a git option or a `<helper>::<address>` transport helper before cloning. Two copies of a security check drift: the second one arrived four months after the first, and only because someone went looking for it.

The rule now lives in omarchy-git-url-check and the callers ask it. Its absence refuses the URL rather than waving it through, since the callers read a non-zero status as a refusal and a missing command exits 127.

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

* Refuse a git URL naming a transport Omarchy does not clone from

`<helper>::<address>` is only one of the two ways a URL reaches a remote helper. git also resolves git-remote-<scheme> for `<scheme>://<address>` whenever the scheme is not one it connects itself, so `ext::sh -c id` and `ext://sh -c id` arrive at the same helper while only the first was refused.

That shape cannot be refused outright, because it is also how every legitimate URL arrives, so the scheme is checked against the transports git still connects itself. `git+ssh` and `ssh+git` are on that list: they are spelled like a helper and read as plain ssh, and leaving them off would refuse a URL that clones today. `ext` and `fd` are off it deliberately -- git ships a helper for each, and `ext` runs whatever command the URL carries.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
2026-08-25 09:10:20 +02:00
BastiandGitHub 30471bf35a Guard plugin-add against git transport-helper URLs (match theme-install) (#8067)
* Guard plugin-add against git transport-helper URLs

omarchy-plugin-add cloned a user-supplied git URL without the
transport-helper guard that omarchy-theme-install already applies
(added in #7884, which did not touch plugin-add). Port that guard
(reject ext::/fd:: and leading-dash forms, keep https/ssh/scp-style
incl. IPv6) and add a regression test. Stock systems are unaffected
(git default protocol.ext.allow=never); this removes the silent
dependency on that default and aligns the two install paths.

* Test the plugin-add guard's leading-dash arm via the gum input path

The prior leading-dash cases only exercised the argv option parser, not
the guard (removing the guard's -* arm left them green). Drive a dash
value through the interactive gum prompt under a pty so the post-input
guard is actually covered; skip cleanly where util-linux script is
unavailable.
2026-08-25 08:29:48 +02:00
Akshar Patel 597f57a198 Allow Resolve Voiceover to release focus 2026-08-24 22:00:42 -04:00
Ryan Hughes e66c27f1e7 Require signed packages from the Omarchy repository 2026-08-24 12:53:24 -04:00
David Helmus 44b00a4e80 test: avoid mise shim recursion 2026-08-24 08:52:25 +02:00
Taksh 7c896d3521 Keep a web app name out of the launcher's directory structure
The app name becomes a filename, and omarchy-webapp-install ran
`mkdir -p "$(dirname "$DESKTOP_FILE")"` over it, so every slash turned into a
directory level. Typing a URL into the Name field -- the reported way in --
wrote the launcher to
`~/.local/share/applications/http:/127.0.0.1:4000/.desktop`.

Removal could then never reach it. The picker lists the file but displays a
name derived from the path, and the removal rebuilt a flat
`$DESKTOP_DIR/$APP_NAME.desktop` from that name, so `rm -f` deleted nothing and
the app stayed in the launcher with no error.

Refuse a name containing a slash rather than silently renaming what the user
typed, and delete the file the scan actually found instead of a path rebuilt
from its display name. The second half also clears up whatever earlier versions
nested, which a reconstructed path cannot address.
2026-08-24 07:16:53 +05:30
Akshar Patel 94a7b70ed4 Explain Resolve pointer focus override 2026-08-14 23:50:12 -04:00
Akshar Patel ec9da050ee Stop Resolve dialogs from recapturing pointer focus 2026-08-14 23:47:31 -04:00
129 changed files with 5478 additions and 146 deletions
+2 -2
View File
@@ -2,7 +2,7 @@
面向中国开发者的 [Omarchy](https://omarchy.org) 下游发行版:完整保留 Omarchy 的 Arch + Hyprland 桌面体验,为中国网络环境、中文使用习惯与国内 AI 服务做系统级增强。
当前版本 `4.0.0.alpha-cn.6`(基于上游 `quattro` 分支构建的基线 ISO)。
当前版本 `4.0.0.alpha-cn.8`(基于上游 `quattro` 分支构建的基线 ISO)。
## 特性
@@ -19,7 +19,7 @@
- **中文环境开箱即用**zh_CN locale、思源黑体 / 宋体字体栈、高分屏分数缩放预设
- **中文输入法**Fcitx5 + Rime 预配置,Wayland / GTK / Qt / Electron 全栈兼容,快捷键冲突自动处理
- **AI Hub**Kimi、DeepSeek、Z.AI/GLM、MiniMax 与本地 Ollama 的一等 Provider 支持,与 Claude Code、Codex、OpenCode、Kimi Code、Deep Code 等 Harness 的统一配置向导、凭据安全存储与连接诊断;另有 Dim、DSH 等自管登录的 Agent CLI 一键安装
- **国内应用中心**:微信、企业微信、QQ、飞书、钉钉、腾讯会议、WPS 等应用的可信安装入口
- **国内应用中心**:微信、QQ、飞书、钉钉、腾讯会议、WPS 等应用的可信安装入口
- **一键转换**:原版 Omarchy(包安装)一条命令转成 OmarchyCN,logo/菜单/键位/输入法/软件源全套切换,`omarchy cn revert` 一键还原
- **Overlay 安装器**:在现有 Omarchy checkout 上叠加 OmarchyCN,全程可逆、可卸载
- **统一诊断**`omarchycn doctor` 覆盖网络、镜像、输入法、显示与 AI 配置
+1
View File
@@ -41,6 +41,7 @@ GROUP_DESCRIPTIONS[clipboard]="Clipboard helpers"
GROUP_DESCRIPTIONS[cmd]="Command and shortcut helpers"
GROUP_DESCRIPTIONS[cn]="OmarchyCN China environment integration"
GROUP_DESCRIPTIONS[config]="System configuration helpers"
GROUP_DESCRIPTIONS[crash]="Crash notification controls"
GROUP_DESCRIPTIONS[debug]="Diagnostics and support logs"
GROUP_DESCRIPTIONS[finalize]="Finalize user setup"
GROUP_DESCRIPTIONS[default]="Default application selection"
+3 -1
View File
@@ -92,8 +92,10 @@ omp)
;;
ori)
# Ori is a harness launcher, and `ori code` is the agent it runs itself.
# A prompt alone means one headless turn there, printed after the turn ends,
# so --interactive is what seeds the session with it and keeps the window.
command=(ori code)
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
[[ -n ${prompt:-} ]] && command+=(--interactive --prompt "$prompt")
;;
pi)
command=(pi)
+1 -1
View File
@@ -528,7 +528,7 @@ def fetch_codex_rpc():
try:
proc = subprocess.Popen(
[codex, "-s", "read-only", "-a", "untrusted", "app-server"],
[codex, "-s", "read-only", "-a", "on-request", "app-server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
+17 -4
View File
@@ -4,7 +4,13 @@
# omarchy:args=[--no-osd] [+N%|N%-|N%]
# omarchy:examples=omarchy brightness display apple | omarchy brightness display apple +5% | omarchy brightness display apple --no-osd 50%
device_cache="${XDG_RUNTIME_DIR:-/tmp}/omarchy-brightness-display-apple.device"
# Only cache under the user-private runtime dir. With no XDG_RUNTIME_DIR we skip
# caching (detect every run) rather than fall back to a predictable, world-writable
# /tmp path another user could pre-create.
device_cache=""
if [[ -n ${XDG_RUNTIME_DIR:-} ]]; then
device_cache="$XDG_RUNTIME_DIR/omarchy-brightness-display-apple.device"
fi
no_osd=0
if [[ ${1:-} == "--no-osd" ]]; then
no_osd=1
@@ -28,9 +34,14 @@ find_apple_display_device() {
local cached=""
local device=""
if [[ -r $device_cache ]]; then
if [[ -n $device_cache && -r $device_cache ]]; then
read -r cached <"$device_cache" || true
if [[ -n $cached && -e $cached ]]; then
# Trust a cached value only if it still names a hiddev character device. A
# stale or unexpected cache (a regular file, a non-hiddev node) is ignored and
# we re-detect instead of handing an arbitrary path to asdcontrol. The globs
# are left unquoted on purpose: [[ ]] pattern-matches an unquoted right side,
# and quoting them would turn the match into a literal string comparison.
if [[ ( $cached == /dev/hiddev* || $cached == /dev/usb/hiddev* ) && -c $cached ]]; then
printf '%s\n' "$cached"
return 0
fi
@@ -39,7 +50,9 @@ find_apple_display_device() {
device="$(detect_apple_display_device)" || return 1
[[ -n $device ]] || return 1
printf '%s\n' "$device" >"$device_cache"
if [[ -n $device_cache ]]; then
printf '%s\n' "$device" >"$device_cache"
fi
printf '%s\n' "$device"
}
+7 -5
View File
@@ -78,14 +78,16 @@ check_omarchy_repo() {
echo "INFO omarchy-repo: 非 stable 通道,跳过 OmarchyCN 镜像检查"
return 0
fi
if cn_mirror_omarchy_repo_ok; then
# --fix runs the helper even when the line is present: it also heals a missing key
if [[ $fix == "true" ]]; then
if cn_mirror_omarchy_repo_fix && cn_mirror_omarchy_repo_ok; then
echo "PASS omarchy-repo: OmarchyCN 镜像行与 registry 公钥在位"
return 0
fi
elif cn_mirror_omarchy_repo_ok; then
echo "PASS omarchy-repo: OmarchyCN 镜像行在位"
return 0
fi
if [[ $fix == "true" ]] && cn_mirror_omarchy_repo_fix && cn_mirror_omarchy_repo_ok; then
echo "PASS omarchy-repo: 已重新插入 OmarchyCN 镜像行"
return 0
fi
echo "FAIL omarchy-repo: [omarchy] 缺 OmarchyCN 镜像行(omarchycn doctor mirror --fix 可修复)"
failures=$((failures + 1))
}
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
# omarchy:summary=Silence crash notifications for one program, or list what is silenced
# omarchy:args=[--] [<program>] [on|off|toggle]
# omarchy:examples=omarchy crash mute | omarchy crash mute hyprland | omarchy crash mute /usr/bin/hyprland | omarchy crash mute hyprland off
# The flag omarchy-crash-watch reads before announcing a crash. Muting is per
# program; Trigger > Toggle > Crash Capture is the switch for all of them.
set -uo pipefail
readonly MUTES="$HOME/.local/state/omarchy/toggles/crash-ignore"
usage() {
echo "Usage: omarchy crash mute [--] [<program>] [on|off|toggle]" >&2
}
# Only regular files, because that is all the watcher honours: anything else in
# there would be reported as muted while the crashes kept arriving. The dotted
# glob is for a program legitimately called .hidden, and `.` and `..` fail the
# same -f test that keeps them out.
list() {
local entry found=0
for entry in "$MUTES"/* "$MUTES"/.*; do
[[ -f $entry ]] || continue
printf '%s\n' "${entry##*/}"
found=1
done
((found)) || echo "No programs muted. Crashes all notify."
}
# A program may be named -h, and the router answers that with its own help
# before this ever runs. `omarchy crash mute -- -h` is the way through.
[[ ${1:-} == "--" ]] && shift
if (($# == 0)); then
list
exit 0
fi
program=$1
action=${2:-on}
# The watcher keys the mute on the executable's basename, so accept the path it
# reports as readily as the name, and reduce either the same way it does.
program=${program##*/}
if [[ -z $program || $program == "." || $program == ".." ]]; then
echo "Not a program name: $1" >&2
usage
exit 1
fi
case "$action" in
on|off|toggle) ;;
*)
echo "Not an action: $action" >&2
usage
exit 1
;;
esac
omarchy-toggle "crash-ignore/$program" "$action" || exit 1
# Report what is now true rather than what was asked for: the flag is what the
# watcher reads, and a toggle does not say which way it went.
if omarchy-toggle-enabled "crash-ignore/$program"; then
echo "Muted crash notifications for $program."
else
echo "Crash notifications for $program are back on."
fi
+28 -5
View File
@@ -48,12 +48,17 @@ announce() {
# -n 0 so a restart does not re-announce crashes already dealt with.
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
while IFS= read -r entry; do
# A dash for a field that is empty as well as one that is missing: tab is
# IFS whitespace, so an empty field collapses into the next delimiter and
# every field after it shifts along one. A process can set its own comm to
# nothing, and that crash used to be read as somebody else's and dropped.
IFS=$'\t' read -r uid comm pid exe signal < <(
jq -r '[(._UID // "-"),
(.COREDUMP_COMM // "-"),
(.COREDUMP_PID // "-"),
(.COREDUMP_EXE // "-"),
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
jq -r 'def field: if . == null or . == "" then "-" else . end;
[(._UID | field),
(.COREDUMP_COMM | field),
(.COREDUMP_PID | field),
(.COREDUMP_EXE | field),
(.COREDUMP_SIGNAL_NAME | field)] | @tsv' <<<"$entry" 2>/dev/null
)
[[ $pid =~ ^[0-9]+$ ]] || continue
@@ -71,11 +76,29 @@ journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
name=$comm
[[ $exe == /* ]] && name=${exe##*/}
# A process can set its own comm to anything prctl takes, slashes included,
# and a crash with no recorded executable falls back to it. The mute below
# turns this name into a path, so keep it one component: a crash must not
# reach a flag outside crash-ignore/, nor have a diagnosis write one there.
name=${name##*/}
# What that leaves is not always a name. "/" leaves nothing, which is no
# kind of array subscript and no kind of toast; a dot component names a
# directory rather than a flag, so a mute on it would touch that directory
# and then never match; and a dash is what the read above puts there when
# the crash recorded no name at all.
[[ -n $name && $name != "-" && $name != "." && $name != ".." ]] || name=unknown
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
# Never announce our own machinery, or it notifies about itself.
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
# Muted at the end of a diagnosis, when the user was offered it and said
# yes. A flag per program rather than one list, so omarchy-crash-mute can
# lift one without reading, rewriting and re-parsing the rest.
omarchy-toggle-enabled "crash-ignore/$name" && continue
now=$EPOCHSECONDS
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
+12
View File
@@ -6,6 +6,18 @@
set -euo pipefail
# Whenever this runs as root — invoked directly through the passwordless
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
# prepends a user-writable checkout bin/ to it. Every helper this script calls
# by bare name (dirname, install, tee, rm, nmcli, systemctl, awk) is a system
# tool, never an omarchy-* command, so pin PATH to trusted system directories
# and keep root from resolving one out of that checkout. The unprivileged
# wrapper phase keeps the caller's PATH so it can still find sudo/pkexec.
if (( EUID == 0 )); then
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
fi
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
provider_from_arg() {
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# omarchy:summary=Check that a git URL names a repository, not a transport helper
# omarchy:args=<git-url>
# omarchy:hidden=true
set -euo pipefail
# git picks a remote helper -- an executable it runs at clone time -- out of a URL
# in exactly two shapes, and no others: `<helper>::<address>`, and
# `<scheme>://<address>` for any scheme git does not handle itself. A single
# colon is always scp-style ssh, and a bare path is always a path; neither can
# reach a helper. So constraining those two shapes covers the whole surface.
#
# The `::` shape is refused outright, because no helper reachable that way is one
# a theme or plugin URL has business naming, and `ext::` runs a shell command.
# The `://` shape cannot be refused the same way, since it is also how every
# legitimate URL arrives -- so it is allowlisted instead. The list is the
# transports git still connects itself, `git+ssh` and `ssh+git` included: those
# two are spelled like a helper but are read as plain ssh. `ext` and `fd` are
# left out deliberately -- git ships a helper for each, and `ext` runs whatever
# command the URL carries.
TRANSPORTS=(ssh git git+ssh ssh+git http https ftp ftps file)
fail() {
echo "omarchy-git-url-check: $*" >&2
exit 1
}
url="${1-}"
if [[ -z $url ]]; then
fail "a git URL is required"
fi
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
fail "'$url' names a git option or transport helper, not a repository."
fi
if [[ $url =~ ^([A-Za-z0-9][A-Za-z0-9+.-]*):// ]]; then
scheme="${BASH_REMATCH[1]}"
for transport in "${TRANSPORTS[@]}"; do
if [[ $scheme == "$transport" ]]; then
exit 0
fi
done
fail "'$url' names the '$scheme' transport, which Omarchy does not clone from."
fi
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# omarchy:summary=Match the Dell XPS 13 DX13260 that requires the sidecar amplifier workaround.
product_sku="${OMARCHY_DMI_PRODUCT_SKU:-/sys/class/dmi/id/product_sku}"
omarchy-hw-match "DX13260" &&
grep -qix "0E53" "$product_sku" 2>/dev/null
+8
View File
@@ -11,6 +11,14 @@ MONITOR_LUA="$HOME/.config/hypr/monitors.lua"
INTERNAL=$(omarchy-hyprland-monitor-laptop)
# INTERNAL is written into generated Lua and hyprctl eval/dispatch below, so a
# name that is not a plain connector string could execute on the next reload.
# Names come from hyprctl; a user-created headless output can carry anything.
if [[ -n $INTERNAL && ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Refusing unsafe internal monitor name" >&2
exit 1
fi
valid_scale() {
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
}
+7
View File
@@ -28,6 +28,13 @@ off() {
exit 1
fi
# The name is written into generated Lua below, so only a plain connector
# name may pass; anything else could execute on the next reload.
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
omarchy-notification-send -g 󰍹 "Refusing unsafe monitor name"
exit 1
fi
if ! omarchy-hyprland-monitor-external-active; then
omarchy-notification-send -g 󰍹 "Can't disable the only active display"
exit 1
@@ -22,6 +22,15 @@ on() {
exit 1
fi
# Both names are written into generated Lua below, so only plain connector
# names may pass; a user-created headless output can carry any name.
for output in "$INTERNAL" "$EXTERNAL"; do
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
omarchy-notification-send -g 󰍹 "Refusing unsafe monitor name"
exit 1
fi
done
omarchy-hyprland-toggle $DISABLE_TOGGLE off
if omarchy-hyprland-toggle-disabled $TOGGLE; then
+8
View File
@@ -80,6 +80,14 @@ set_scale() {
local width="$(echo "$monitor_info" | jq -r '.width')"
local height="$(echo "$monitor_info" | jq -r '.height')"
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
# active_monitor is written into the Lua string eval'd below, so only a plain
# connector name may pass; a hostile output name could execute otherwise.
if [[ ! $active_monitor =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Refusing unsafe monitor name" >&2
exit 1
fi
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
# factor even when the monitor scale itself is fractional.
+11 -17
View File
@@ -6,9 +6,10 @@
set -e
setup_policy_directory() {
sudo mkdir -p "$1"
sudo chmod a+rw "$1"
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
setup_chromium_policy_directory() {
browser_policy_setup_dir "$1"
}
announce_browser_installed() {
@@ -23,13 +24,6 @@ copy_chromium_flags() {
omarchy-install-chromium-ytdlp
}
setup_firefox_preferences() {
local distribution_dir="$1"
setup_policy_directory "$distribution_dir"
sudo cp -f "$OMARCHY_PATH/default/firefox/policies.json" "$distribution_dir/policies.json"
}
setup_firefox_wayland() {
mkdir -p ~/.config/environment.d
echo "MOZ_ENABLE_WAYLAND=1" > ~/.config/environment.d/omarchy-firefox-wayland.conf
@@ -40,7 +34,7 @@ chromium)
echo "Installing Chromium..."
omarchy-pkg-add chromium
setup_policy_directory /etc/chromium/policies/managed
setup_chromium_policy_directory /etc/chromium/policies/managed
copy_chromium_flags ~/.config/chromium-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Chromium"
@@ -49,7 +43,7 @@ chrome)
echo "Installing Chrome..."
omarchy-pkg-aur-add google-chrome || exit 1
setup_policy_directory /etc/opt/chrome/policies/managed
setup_chromium_policy_directory /etc/opt/chrome/policies/managed
copy_chromium_flags ~/.config/chrome-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Chrome"
@@ -58,7 +52,7 @@ edge)
echo "Installing Edge..."
omarchy-pkg-aur-add microsoft-edge-stable-bin || exit 1
setup_policy_directory /etc/opt/edge/policies/managed
setup_chromium_policy_directory /etc/opt/edge/policies/managed
copy_chromium_flags ~/.config/microsoft-edge-stable-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Edge"
@@ -67,7 +61,7 @@ brave)
echo "Installing Brave..."
omarchy-pkg-aur-add brave-bin || exit 1
setup_policy_directory /etc/brave/policies/managed
setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Brave"
@@ -76,7 +70,7 @@ brave-origin)
echo "Installing Brave Origin..."
omarchy-pkg-aur-add brave-origin-bin || exit 1
setup_policy_directory /etc/brave/policies/managed
setup_chromium_policy_directory /etc/brave/policies/managed
copy_chromium_flags ~/.config/brave-origin-flags.conf
omarchy-theme-set-browser
announce_browser_installed "Brave Origin"
@@ -85,7 +79,7 @@ firefox)
echo "Installing Firefox..."
omarchy-pkg-add firefox || exit 1
setup_firefox_preferences /usr/lib/firefox/distribution
browser_policy_setup_firefox_distribution /usr/lib/firefox/distribution
setup_firefox_wayland
announce_browser_installed "Firefox"
;;
@@ -93,7 +87,7 @@ zen)
echo "Installing Zen..."
omarchy-pkg-aur-add zen-browser-bin || exit 1
setup_firefox_preferences /opt/zen-browser/distribution
browser_policy_setup_firefox_distribution /opt/zen-browser/distribution
setup_firefox_wayland
announce_browser_installed "Zen"
;;
+1 -1
View File
@@ -10,4 +10,4 @@ echo "Enabling ONCE background service..."
sudo systemctl enable --now once-background.service
echo -e "\nLaunching ONCE..."
once
sudo once
+7
View File
@@ -93,6 +93,13 @@ if [[ -z $url ]]; then
[[ -n $url ]] || fail "a git URL is required"
fi
# Refuse a URL that names a git option or a transport helper before cloning, so
# an untrusted URL cannot run a command before the plugin is validated or
# enabled. The check is shared with omarchy-theme-install and explains itself; a
# missing checker leaves this non-zero, which refuses the URL rather than
# cloning it.
omarchy-git-url-check "$url" || exit 1
if (( ! ASSUME_YES )); then
cat >&2 <<WARN
+6
View File
@@ -742,6 +742,12 @@ create_user() {
# for specific commands), and a duplicate grant is harmless.
echo "%wheel ALL=(ALL:ALL) ALL" >/etc/sudoers.d/00-omarchy-wheel
chmod 440 /etc/sudoers.d/00-omarchy-wheel
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
done
}
install_authorized_keys() {
+7 -2
View File
@@ -24,9 +24,14 @@ echo -e "\e[32mRemoving FIDO2 device from authentication.\n\e[0m"
remove_pam_config
if [[ -d /etc/fido2 ]]; then
authdir=/etc/fido2
# -d follows symlinks, so a dangling link at /etc/fido2 would survive this and
# a later setup would install the authfile through it. rm -rf on a symlink
# removes the link itself, never the directory it points at.
if [[ -e $authdir || -L $authdir ]]; then
echo "Removing FIDO2 configuration..."
sudo rm -rf /etc/fido2
sudo rm -rf "$authdir"
fi
echo "Removing FIDO2 packages..."
+71 -4
View File
@@ -4,6 +4,7 @@
# omarchy:requires-sudo=true
set -e
set -o pipefail
check_fido2_hardware() {
@@ -50,13 +51,79 @@ if ! check_fido2_hardware; then
fi
# Create the pamu2fcfg file
if [[ ! -f /etc/fido2/fido2 ]]; then
sudo mkdir -p /etc/fido2
authdir=/etc/fido2
authfile=/etc/fido2/fido2
# install -d follows a symlink here and applies the mode and ownership to
# whatever it points at, so the credential would be staged and published inside
# the link target and that directory reopened to root:root 755. This is the
# threat omarchy-remove-security-fido2 already names on its side.
if [[ -L $authdir || ( -e $authdir && ! -d $authdir ) ]]; then
echo -e "\e[31m\n$authdir is not a FIDO2 configuration directory.\e[0m"
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
exit 1
fi
# -f follows symlinks, so the already-registered check below reads a symlinked
# authfile as a registration and leaves it in place, and is false for a
# directory, so it tries to register over one. Only a regular file is a valid
# pam_u2f authfile.
if [[ -L $authfile || ( -e $authfile && ! -f $authfile ) ]]; then
echo -e "\e[31m\n$authfile is not a FIDO2 registration file.\e[0m"
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
exit 1
fi
if [[ ! -f $authfile ]]; then
sudo install -d -m 755 -o root -g root "$authdir"
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
echo -e "Touch your FIDO2 key when it lights up...\n"
if pamu2fcfg >/tmp/fido2; then
sudo mv /tmp/fido2 /etc/fido2/fido2
# A unique sibling created by root cannot be replaced by another process
# running as this user. Stream pamu2fcfg into it instead of asking root to
# reopen a caller-owned path: an observed temporary name could otherwise be
# replaced with a symlink before the privileged copy. The final rename is
# atomic, and -T refuses a directory at the destination. Mode 644 keeps the
# root-owned global authfile readable when pam_u2f uses openasuser; only root
# can still rewrite it.
stage=""
# mktemp's output is an operand for four privileged commands below, one of
# them an rm. Take only the name this script asked for rather than whatever
# came back on stdout.
safe_stage_path() {
local candidate=$1
local prefix="$authfile.new."
local suffix
[[ $candidate == "$prefix"* ]] || return 1
suffix=${candidate#"$prefix"}
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
}
cleanup_stage() {
local status=$?
if safe_stage_path "$stage"; then
sudo rm -f -- "$stage" || true
fi
return "$status"
}
trap cleanup_stage EXIT
stage=$(sudo mktemp "$authfile.new.XXXXXX")
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
echo -e "\e[31m\nCould not create a safe staging file beside $authfile.\e[0m"
exit 1
fi
if pamu2fcfg | sudo tee "$stage" >/dev/null && [[ -s $stage ]]; then
sudo chmod 644 "$stage"
sudo mv -Tf "$stage" "$authfile"
stage=""
trap - EXIT
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
else
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
+4 -8
View File
@@ -16,14 +16,10 @@ if [[ -z $REPO_URL ]]; then
exit 1
fi
# git reads a leading dash as an option, and `<helper>::<address>` as a remote
# helper to run. The helper name is a bare word at the very start, which is what
# this matches; an scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git
# carries `::` too and must still clone.
if [[ $REPO_URL == -* || $REPO_URL =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
echo "Error: '$REPO_URL' names a git option or transport helper, not a repository."
exit 1
fi
# Refuse a URL that names a git option or a transport helper before cloning. The
# check is shared with omarchy-plugin-add and explains itself; a missing checker
# leaves this non-zero, which refuses the URL rather than cloning it.
omarchy-git-url-check "$REPO_URL" || exit 1
THEMES_DIR="$HOME/.config/omarchy/themes"
+9 -19
View File
@@ -3,23 +3,15 @@
# omarchy:summary=Apply the current theme color to Chromium, Chrome, Edge, and Brave
# omarchy:hidden=true
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
CHROMIUM_THEME=$HOME/.local/state/omarchy/current/theme/chromium.theme
THEME_HEX_COLOR=$BROWSER_POLICY_DEFAULT_COLOR
if [[ -f $CHROMIUM_THEME ]]; then
THEME_RGB_COLOR=$(<$CHROMIUM_THEME)
THEME_HEX_COLOR=$(printf '#%02x%02x%02x' ${THEME_RGB_COLOR//,/ })
else
# Use a default, neutral grey if theme doesn't have a color
THEME_HEX_COLOR="#1c2027"
THEME_HEX_COLOR=$(browser_policy_theme_hex "$(<$CHROMIUM_THEME)")
fi
set_browser_policy() {
local policy_dir="$1"
[[ -d $policy_dir ]] || return
echo "{\"BrowserThemeColor\": \"$THEME_HEX_COLOR\", \"BrowserColorScheme\": \"device\"}" | tee "$policy_dir/color.json" >/dev/null
}
refresh_running_browser() {
local process="$1"
local command="$2"
@@ -30,17 +22,15 @@ refresh_running_browser() {
fi
}
set_browser_policy /etc/chromium/policies/managed
failed=0
omarchy-theme-set-browser-policy "${THEME_HEX_COLOR#\#}" || failed=1
refresh_running_browser chromium chromium
set_browser_policy /etc/opt/chrome/policies/managed
refresh_running_browser chrome google-chrome-stable || refresh_running_browser chrome google-chrome
set_browser_policy /etc/opt/edge/policies/managed
refresh_running_browser msedge microsoft-edge-stable
set_browser_policy /etc/brave/policies/managed
refresh_running_browser brave brave
# Match on the binary path: the running process is named plain "brave", and a
# bare -f brave-origin pattern would also match the installer's own terminal.
refresh_running_browser /opt/brave-origin-bin/ brave-origin -f
exit "$failed"
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# omarchy:summary=Write the current theme color into the browser policy directories
# omarchy:args=<rrggbb>
# omarchy:hidden=true
set -euo pipefail
# Whenever this runs as root — invoked directly through the passwordless
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
# prepends a user-writable checkout bin/ to it. Every helper this script calls
# by bare name (printf's builtin aside: install, mktemp, rm) is a system tool,
# never an omarchy-* command, so pin PATH to trusted system directories and keep
# root from resolving one out of that checkout. The unprivileged wrapper phase
# keeps the caller's PATH so it can still find sudo/pkexec.
if (( EUID == 0 )); then
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
fi
# Enterprise policy trust roots. The list is fixed here rather than taken from
# the caller: the caller chooses a color, never a path.
POLICY_DIRS=(
/etc/chromium/policies/managed
/etc/opt/chrome/policies/managed
/etc/opt/edge/policies/managed
/etc/brave/policies/managed
)
# The path etc/sudoers.d/omarchy-theme-browser names. The privileged half always
# runs from there rather than from whichever copy was invoked, so the rule
# matches even where $OMARCHY_PATH points at a checkout.
PACKAGED_PATH=/usr/bin/omarchy-theme-set-browser-policy
usage() {
echo "Usage: omarchy-theme-set-browser-policy <rrggbb>" >&2
}
if (( $# != 1 )); then
usage
exit 1
fi
color="$1"
# Six lowercase hex digits is the whole of what this accepts. The leading "#"
# is added when the JSON is written rather than passed in: "#" opens a comment
# in sudoers, and keeping it out of argv lets the sudoers rule spell the
# argument as a plain six-character glob.
if [[ ! $color =~ ^[0-9a-f]{6}$ ]]; then
echo "omarchy-theme-set-browser-policy: expected six lowercase hex digits, got '$color'" >&2
exit 1
fi
# True when sudo would run this exact command without stopping for a password.
# `sudo -l` on its own reports whether a command is permitted, which the blanket
# %wheel rule answers yes to for everything; the long listing prints the matched
# entry's tags, so !authenticate is the grant in
# etc/sudoers.d/omarchy-theme-browser and nothing else. Listing runs nothing
# and, under -n, prompts for nothing.
sudo_grants_passwordless() {
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
}
require_root() {
if (( EUID == 0 )); then
return
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
exec sudo "$PACKAGED_PATH" "$@"
else
exec pkexec "$PACKAGED_PATH" "$@"
fi
}
require_root "$color"
failed=0
staged=""
# Bash 5.3 makes the EXIT trap's last command decide the script's exit status,
# so this handler must not end on a false test. Every successful run clears
# staged, and a trailing `[[ -n $staged ]] && ...` would report that as failure.
cleanup() {
if [[ -n $staged ]]; then
rm -f "$staged"
fi
}
trap cleanup EXIT
for policy_dir in "${POLICY_DIRS[@]}"; do
# Only browsers Omarchy has installed have a policy directory. Creating one
# here would hand a browser a managed-policy root it does not otherwise have.
[[ -d $policy_dir && ! -L $policy_dir ]] || continue
dest=$policy_dir/color.json
staged=$(mktemp) || {
failed=1
continue
}
printf '{"BrowserThemeColor": "#%s", "BrowserColorScheme": "device"}\n' "$color" >"$staged"
if [[ -L $dest || -d $dest ]]; then
if ! rm -rf -- "$dest"; then
rm -f "$staged"
staged=""
echo "omarchy-theme-set-browser-policy: cannot replace $dest" >&2
failed=1
continue
fi
fi
if ! install -m 0644 -o root -g root -T "$staged" "$dest"; then
rm -f "$staged"
staged=""
echo "omarchy-theme-set-browser-policy: cannot write $dest" >&2
failed=1
continue
fi
rm -f "$staged"
staged=""
done
exit "$failed"
+40 -14
View File
@@ -7,44 +7,70 @@
KIND="${1:-}"
ACTION="${2:-toggle}"
usage() {
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
}
case "$KIND" in
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
*)
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
usage
exit 1
;;
esac
# Hyprland sources this directory on reload, so the disabled state survives restarts
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled.lua"
# The persisted disable is the device name stored as plain data; on every
# reload default/hypr/disabled-input-device.lua reads it back and disables the
# device. Names come from USB descriptors and must not be interpolated into
# shell or Lua. The path is hardcoded to ~/.local/state like the sibling
# toggle tools, so it keeps working when XDG_STATE_HOME diverges.
NAME_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled-name"
device="$("omarchy-hw-$KIND")"
if [[ -z $device ]]; then
echo "No $KIND device found" >&2
exit 1
fi
require_device() {
if [[ -z $device ]]; then
echo "No $KIND device found" >&2
exit 1
fi
if [[ $device == *[[:cntrl:]]* ]]; then
echo "Invalid $KIND device name" >&2
exit 1
fi
}
apply_device() {
local enabled=$1
local quoted=${device//\\/\\\\}
quoted=${quoted//\"/\\\"}
hyprctl eval "hl.device({ name = \"$quoted\", enabled = $enabled })" >/dev/null
}
enable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
rm -f "$STATE_FILE"
# Clear the persisted state before requiring a usable device, so a device
# that stops reporting a valid name can never wedge the disable in place.
rm -f "$NAME_FILE"
require_device
apply_device true
omarchy-osd -i "$ICON" -m "$LABEL enabled"
}
disable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
mkdir -p "$(dirname "$STATE_FILE")"
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
require_device
apply_device false
mkdir -p "$(dirname "$NAME_FILE")"
printf '%s\n' "$device" >"$NAME_FILE"
omarchy-osd -i "$ICON" -m "$LABEL disabled"
}
case "$ACTION" in
on) enable ;;
off) disable ;;
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
toggle) if [[ -f $NAME_FILE ]]; then enable; else disable; fi ;;
*)
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
usage
exit 1
;;
esac
+20 -1
View File
@@ -1312,7 +1312,21 @@ apply_system_transition() {
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
as_root gtk-update-icon-cache /usr/share/icons/Yaru >/dev/null 2>&1 || true
as_root install -d -m 0777 /etc/chromium/policies/managed
local browser_policy_helper=/usr/share/omarchy/install/helpers/browser-policy.sh
if ! as_root test -f "$browser_policy_helper"; then
warn "$browser_policy_helper is unavailable; Chromium policy directories were not hardened."
else
as_root env OMARCHY_PATH=/usr/share/omarchy \
bash -euo pipefail -c '
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
browser_policy_setup_dir /etc/chromium/policies/managed
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ $dir == "/etc/chromium/policies/managed" ]] && continue
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
done
'
fi
as_root install -d -m 0755 /usr/lib/chromium
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
@@ -2302,6 +2316,11 @@ refresh_current_theme_after_upgrade() {
# hooks because one of them runs `hyprctl reload`. Still poke terminal
# emulators so the active upgrade terminal picks up generated theme files.
run_as_user_omarchy omarchy-restart-terminal >/dev/null 2>&1 || true
# apply_system_transition purged user-owned color.json. Headless theme-set
# skipped omarchy-theme-set-browser, so rewrite the colour here.
run_as_user_omarchy omarchy-theme-set-browser >/dev/null 2>&1 ||
warn "Could not apply browser theme colour. Run 'omarchy theme set \"$theme_name\"' after reboot if Chromium's theme looks stale."
}
# Everything below mutates the system, so a non-zero exit from here on leaves a
+18 -2
View File
@@ -13,6 +13,18 @@ safe_icon_name() {
| sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//'
}
require_plain_name() {
# The name becomes a filename. A slash would turn it into directory levels, so
# the launcher lands somewhere omarchy-webapp-remove cannot address and the app
# is stuck in the launcher; a leading ../ leaves the applications directory
# altogether. Refuse rather than silently renaming what the user typed -- most
# often it is a URL entered in the name field.
if [[ $1 == */* ]]; then
echo "App name cannot contain '/': $1"
exit 1
fi
}
icon_name_from_ref() {
local ref="$1"
local name
@@ -68,6 +80,7 @@ fetch_site_icon() {
if (( $# < 3 )); then
echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m"
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
require_plain_name "$APP_NAME"
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
APP_URL="https://$APP_URL"
@@ -104,6 +117,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then
exit 1
fi
require_plain_name "$APP_NAME"
if [[ -z $ICON_REF ]]; then
ICON_VALUE=$(safe_icon_name "$APP_NAME")
mkdir -p "$ICON_DIR"
@@ -132,8 +147,9 @@ fi
EXEC_COMMAND="${CUSTOM_EXEC:-omarchy-launch-webapp $APP_URL}"
# Create application .desktop file
DESKTOP_FILE="$HOME/.local/share/applications/$APP_NAME.desktop"
mkdir -p "$(dirname "$DESKTOP_FILE")"
DESKTOP_DIR="$HOME/.local/share/applications"
DESKTOP_FILE="$DESKTOP_DIR/$APP_NAME.desktop"
mkdir -p "$DESKTOP_DIR"
cat >"$DESKTOP_FILE" <<EOF
[Desktop Entry]
+26 -8
View File
@@ -9,14 +9,31 @@ ICON_DIR="$HOME/.local/share/icons/hicolor/256x256/apps"
OLD_ICON_DIR="$HOME/.local/share/applications/icons"
DESKTOP_DIR="$HOME/.local/share/applications/"
if (( $# == 0 )); then
# Find all web apps
while IFS= read -r -d '' file; do
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
WEB_APPS+=("$(basename "${file%.desktop}")")
fi
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0)
# Always index the launchers, so removal deletes the file that was found rather
# than a path rebuilt from the displayed name. Installs predating the name
# validation could nest the launcher inside directories, and those are exactly
# the ones a reconstructed path cannot reach.
WEB_APP_PATHS=()
while IFS= read -r -d '' file; do
if grep -q '^Exec=.*\(omarchy-launch-webapp\|omarchy-webapp-handler\).*' "$file"; then
WEB_APPS+=("$(basename "${file%.desktop}")")
WEB_APP_PATHS+=("$file")
fi
done < <(find "$DESKTOP_DIR" -name '*.desktop' -print0 2>/dev/null)
# The launcher matching a chosen name, or empty when nothing was indexed under
# it (an app removed between the scan and the pick, say).
path_for_web_app() {
local wanted="$1" i
for i in "${!WEB_APPS[@]}"; do
if [[ ${WEB_APPS[$i]} == "$wanted" ]]; then
printf '%s\n' "${WEB_APP_PATHS[$i]}"
return 0
fi
done
}
if (( $# == 0 )); then
if ((${#WEB_APPS[@]})); then
mapfile -t SORTED_WEB_APPS < <(printf '%s\n' "${WEB_APPS[@]}" | sort)
APP_NAME=$(omarchy-menu-select "Select web app to remove" "${SORTED_WEB_APPS[@]}" -- --width 520 --maxheight 520)
@@ -34,7 +51,8 @@ if [[ -z $APP_NAME ]]; then
fi
icon_name=$(printf '%s\n' "$APP_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^[:alnum:]]\+/-/g; s/^-//; s/-$//')
rm -f "$DESKTOP_DIR/$APP_NAME.desktop"
desktop_file=$(path_for_web_app "$APP_NAME")
rm -f "${desktop_file:-$DESKTOP_DIR/$APP_NAME.desktop}"
rm -f "$ICON_DIR/$icon_name.png" "$ICON_DIR/$APP_NAME.png" "$OLD_ICON_DIR/$APP_NAME.png"
if [[ ${OMARCHY_REMOVE_NOTIFY:-true} != "false" ]]; then
+1 -2
View File
@@ -1,8 +1,7 @@
{
"_verified": "2026-08-28, AUR RPC 实证在维护且未 out-of-date",
"_verified": "2026-08-24, AUR RPC 实证在维护且未 out-of-date",
"apps": {
"wechat": { "name": "微信", "source": "aur", "package": "wechat-universal-bwrap", "license": "proprietary" },
"wecom": { "name": "企业微信", "source": "aur", "package": "com.qq.weixin.work.deepin", "license": "proprietary" },
"qq": { "name": "QQ", "source": "aur", "package": "linuxqq", "license": "proprietary" },
"feishu": { "name": "飞书", "source": "aur", "package": "feishu-bin", "license": "proprietary" },
"dingtalk": { "name": "钉钉", "source": "aur", "package": "dingtalk-bin", "license": "proprietary" },
+29
View File
@@ -1,17 +1,31 @@
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<!-- 每条 lang 规则成对出现:50-omarchy.conf 先以 assign 把 generic 名整体替换成
Liberation Sans/Serif 与 JetBrainsMono Nerd Font(且先于本文件加载),
所以除 generic 名外还必须测试替换后的具体家族,否则规则静默失效(Issue #4 -->
<!-- 日文/韩文标签内容保持原生字形(仅约束 generic 请求,避免波及未标签路径) -->
<match target="pattern">
<test name="lang" compare="contains"><string>ja</string></test>
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK JP</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>ja</string></test>
<test name="family"><string>Liberation Sans</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK JP</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>ko</string></test>
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK KR</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>ko</string></test>
<test name="family"><string>Liberation Sans</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK KR</string></edit>
</match>
<!-- 简体中文内容强制简体字形 -->
<match target="pattern">
@@ -19,16 +33,31 @@
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK SC</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>zh-cn</string></test>
<test name="family"><string>Liberation Sans</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans CJK SC</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>zh-cn</string></test>
<test name="family"><string>serif</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Serif CJK SC</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>zh-cn</string></test>
<test name="family"><string>Liberation Serif</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Serif CJK SC</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>zh-cn</string></test>
<test name="family"><string>monospace</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans Mono CJK SC</string></edit>
</match>
<match target="pattern">
<test name="lang" compare="contains"><string>zh-cn</string></test>
<test name="family"><string>JetBrainsMono Nerd Font</string></test>
<edit name="family" mode="prepend" binding="strong"><string>Noto Sans Mono CJK SC</string></edit>
</match>
<!-- 无语言标签的任意字体请求:汉字回退到 SC 而非 JPChromium/Electron 常见路径) -->
<match target="pattern">
+3
View File
@@ -14,6 +14,9 @@ cn_mirror_omarchy_repo_ok() {
cn_mirror_omarchy_repo_fix() {
local conf="${OMARCHYCN_PACMAN_CONF:-/etc/pacman.conf}"
# [omarchy] requires signed packages; the mirror re-signs with the registry key
sudo pacman-key --add "$OMARCHY_PATH/cn/keys/omarchycn-registry.asc" > /dev/null || return 1
sudo pacman-key --lsign-key 74DCF57ACD812B24D959F146BD386048867B33B4 > /dev/null || return 1
cn_mirror_omarchy_repo_ok && return 0
sudo sed -i "s|^Server = https://pkgs.omarchy.org/stable/|Server = $CN_OMARCHY_MIRROR\nServer = https://pkgs.omarchy.org/stable/|" "$conf"
}
-3
View File
@@ -1,8 +1,5 @@
echo "Trust the OmarchyCN registry key and put the [omarchy] mirror first"
sudo pacman-key --add "$OMARCHY_PATH/cn/keys/omarchycn-registry.asc"
sudo pacman-key --lsign-key 74DCF57ACD812B24D959F146BD386048867B33B4
# Only the stable channel is mirrored; edge/rc systems keep upstream directly
source "$OMARCHY_PATH/cn/lib/mirror.sh"
cn_mirror_omarchy_repo_fix
+8
View File
@@ -0,0 +1,8 @@
echo "Refresh the CJK fontconfig priority so zh-cn rules survive 50-omarchy.conf (Issue #4)"
# Only users who applied it before carry the broken copy
conf="$HOME/.config/fontconfig/conf.d/64-omarchycn-cjk.conf"
if [[ -f $conf ]]; then
cp "$OMARCHY_PATH/cn/fontconfig/64-omarchycn-cjk.conf" "$conf"
fc-cache -f > /dev/null
fi
+1 -1
View File
@@ -1 +1 @@
7
8
+32 -1
View File
@@ -87,7 +87,38 @@ ambiguous, say so rather than assembling confidence out of guesswork.
**Leave the system as you found it.** Diagnosis reads; it does not fix, tidy, or
reconfigure. The one thing to clean up is your own: delete the core you extracted
above, which is a copy of the crashed process's memory.
above, which is a copy of the crashed process's memory. The single change a
diagnosis may make is the mute below, and only when the user asks for it.
## Offer to stop the notifications for this program
A crash you have explained often keeps happening anyway. Finish by offering to
silence notifications for **that one program**, and never run it unprompted. Say
how to lift it in the same breath, so it is not a one-way door.
```bash
omarchy-crash-mute '<program>' # silence it
omarchy-crash-mute '<program>' off # let it speak again
omarchy-crash-mute # list what is muted
```
Pass the `binary:` path from the crash facts, or the `process:` name where no
binary was recorded; the command reduces either to the name the watcher keys on.
A diagnosis run by hand from `omarchy agent crash <pid>` has neither, so take
them from `coredumpctl info`. Prefer the binary: a process name is truncated to
15 characters and a basename is not, so muting the truncated form matches
nothing, forever, while looking like it worked.
Quote it. The name is whatever the crashed program's author called a file, and a
single quote inside one closes yours and runs the rest as your shell.
The key is a bare name, so anything run through an interpreter is keyed as the
interpreter: muting `python3.13` silences every Python program on the machine.
Say so rather than quietly doing it.
None of this fixes anything, and a mute offered in place of a fix that was within
reach is the wrong answer. For every program rather than one, the switch is
_Trigger > Toggle > Crash Capture_.
## If it is an Omarchy bug
+4 -1
View File
@@ -3,9 +3,12 @@
o.window(".*[Rr]esolve.*", {
float = true,
stay_focused = true,
-- Prevent modal dialog pointer warps when focus follows the mouse.
no_follow_mouse = true,
tag = "-default-opacity",
opacity = "1 1",
})
o.window({ class = ".*[Rr]esolve.*", title = "^DaVinci Resolve( Studio)? - .+$" }, { fullscreen = true })
o.window({ class = ".*[Rr]esolve.*", title = "^(DaVinci Resolve( Studio)? - .+|Project Manager)$" }, { stay_focused = false })
-- Resolve exposes the Voiceover panel under the generic "Dialog" title.
o.window({ class = ".*[Rr]esolve.*", title = "^(DaVinci Resolve( Studio)? - .+|Project Manager|Preferences|Find Directory|Dialog)$" }, { stay_focused = false })
+21
View File
@@ -0,0 +1,21 @@
-- Disable a Hyprland input device whose name was stored as data, not Lua.
-- Device names come from USB descriptors and must never be loaded as code.
local paths = require("default.hypr.paths")
return function(kind)
-- Hardcoded to ~/.local/state to match omarchy-toggle-input-device and the
-- sibling bash toggle tools, which all write there regardless of
-- XDG_STATE_HOME.
local file = io.open(paths.home .. "/.local/state/omarchy/toggles/hypr/" .. kind .. "-disabled-name", "r")
if not file then
return
end
local name = file:read("*l")
file:close()
if name and name ~= "" then
hl.device({ name = name, enabled = false })
end
end
+13 -3
View File
@@ -4,9 +4,19 @@
local home = os.getenv("HOME")
-- A variable that is set but empty means "unset" (XDG Base Directory spec);
-- bash's ${VAR:-fallback} in the sibling tools treats it the same way.
local function env_or(name, fallback)
local value = os.getenv(name)
if value == nil or value == "" then
return fallback
end
return value
end
return {
home = home,
config_home = os.getenv("XDG_CONFIG_HOME") or (home .. "/.config"),
state_home = os.getenv("XDG_STATE_HOME") or (home .. "/.local/state"),
omarchy_path = os.getenv("OMARCHY_PATH") or "/usr/share/omarchy",
config_home = env_or("XDG_CONFIG_HOME", home .. "/.config"),
state_home = env_or("XDG_STATE_HOME", home .. "/.local/state"),
omarchy_path = env_or("OMARCHY_PATH", "/usr/share/omarchy"),
}
+14 -8
View File
@@ -4,6 +4,8 @@
-- Pass a module prefix for normal package.path modules, e.g.
-- require_all.files(paths.omarchy_path .. "/default/hypr/apps", "default.hypr.apps")
-- Pass nil as the prefix when the directory itself has been added to package.path.
-- Pass options.exclude as a set of base names (without ".lua") to skip; a legacy
-- file that must never be loaded as code stays on disk for a migration to remove.
local M = {}
@@ -12,19 +14,23 @@ local function shell_quote(path)
end
function M.files(dir, module_prefix, options)
local exclude = options and options.exclude or {}
local handle = io.popen("find " .. shell_quote(dir) .. " -maxdepth 1 -type f -name '*.lua' -printf '%f\\n' 2>/dev/null | sort")
if handle then
for filename in handle:lines() do
local module = filename:gsub("%.lua$", "")
if module_prefix then
module = module_prefix .. "." .. module
end
local name = filename:gsub("%.lua$", "")
if not exclude[name] then
local module = name
if module_prefix then
module = module_prefix .. "." .. module
end
if options and options.reload then
package.loaded[module] = nil
end
if options and options.reload then
package.loaded[module] = nil
end
require(module)
require(module)
end
end
handle:close()
end
+15 -1
View File
@@ -4,6 +4,20 @@ local require_all = require("default.hypr.require_all")
local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
package.path = toggles_dir .. "/?.lua;" .. package.path
require_all.files(toggles_dir, nil, { reload = true })
-- touchpad-disabled.lua / touchscreen-disabled.lua were generated Lua in older
-- versions and could carry an injected USB device name. They must never be loaded
-- as code again: exclude them so a not-yet-migrated install cannot execute a
-- leftover payload on reload. The migration recovers the name and deletes them.
require_all.files(toggles_dir, nil, {
reload = true,
exclude = {
["touchpad-disabled"] = true,
["touchscreen-disabled"] = true,
},
})
local disabled_input_device = require("default.hypr.disabled-input-device")
disabled_input_device("touchpad")
disabled_input_device("touchscreen")
require("default.hypr.workspace-layouts")
-1
View File
@@ -385,7 +385,6 @@
"omarchycn.ai.doctor": {"icon":"󰨮","label":"AI 诊断","action":"omarchy-launch-floating-terminal-with-presentation omarchy-cn-ai-doctor"},
"omarchycn.apps": {"icon":"󰀻","label":"国内应用"},
"omarchycn.apps.wechat": {"icon":"","label":"微信","disabled":"omarchy-pkg-present wechat-universal-bwrap","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-cn-app-install wechat'"},
"omarchycn.apps.wecom": {"icon":"","label":"企业微信","disabled":"omarchy-pkg-present com.qq.weixin.work.deepin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-cn-app-install wecom'"},
"omarchycn.apps.qq": {"icon":"","label":"QQ","disabled":"omarchy-pkg-present linuxqq","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-cn-app-install qq'"},
"omarchycn.apps.feishu": {"icon":"","label":"飞书","disabled":"omarchy-pkg-present feishu-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-cn-app-install feishu'"},
"omarchycn.apps.dingtalk": {"icon":"","label":"钉钉","disabled":"omarchy-pkg-present dingtalk-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-cn-app-install dingtalk'"},
-1
View File
@@ -26,7 +26,6 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist
[omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch
# Repositories for debug symbol packages.
-1
View File
@@ -26,5 +26,4 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist
[omarchy]
SigLevel = Optional TrustAll
Server = https://pkgs.omarchy.org/edge/$arch
+1 -2
View File
@@ -26,7 +26,6 @@ Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/mirrorlist
[omarchy]
SigLevel = Optional TrustAll
# OmarchyCN mirror first (registry-signed db, key via cn/keys), upstream fallback
# OmarchyCN mirror first (registry-signed db and packages, key via cn/keys), upstream fallback
Server = https://git.zacharyzhang.com/api/packages/ZacharyZhang-NY/arch/omarchy/x86_64
Server = https://pkgs.omarchy.org/stable/$arch
+8
View File
@@ -0,0 +1,8 @@
# Theme switching is a menu action with no terminal to carry a password prompt,
# and it repaints the browser accent on every switch, so this one write must not
# stop for a password. The argument is spelled out as six hex digits rather than
# a wildcard: the grant covers a color and nothing else, and sudoers matches a
# command's arguments exactly, so it cannot be stretched into extra ones. The
# helper revalidates the same shape, since the terminal path does not come
# through this rule.
%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-theme-set-browser-policy [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
+1 -1
View File
@@ -1 +1 @@
%wheel ALL=(root) NOPASSWD: /usr/bin/timedatectl set-timezone *
%wheel ALL=(root) NOPASSWD: /usr/bin/timedatectl ^set-timezone [A-Za-z0-9_+][A-Za-z0-9_+.-]*(/[A-Za-z0-9_+][A-Za-z0-9_+.-]*)*$
+1
View File
@@ -1,4 +1,5 @@
run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
run_logged "$OMARCHY_INSTALL/config/browser-policy.sh"
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh"
run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh"
+2
View File
@@ -0,0 +1,2 @@
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
browser_policy_setup_dir /etc/chromium/policies/managed
-4
View File
@@ -6,10 +6,6 @@ ln -snf /usr/share/icons/Adwaita/symbolic/actions/go-next-symbolic.svg \
/usr/share/icons/Yaru/scalable/actions/go-next-symbolic.svg
gtk-update-icon-cache /usr/share/icons/Yaru &>/dev/null || true
# Chromium policy directory for theme
mkdir -p /etc/chromium/policies/managed
chmod a+rw /etc/chromium/policies/managed
# Default Chromium to follow system appearance ("device") instead of dark
mkdir -p /usr/lib/chromium
echo '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' > \
+4
View File
@@ -25,6 +25,10 @@ run_logged "$OMARCHY_INSTALL/hardware/intel/fred.sh"
run_logged "$OMARCHY_INSTALL/hardware/intel/fix-wifi7-eht.sh"
run_logged "$OMARCHY_INSTALL/hardware/intel/sof-firmware.sh"
# Rebuilds the boot image, so it has to follow the Panther Lake kernel swap
# above rather than sit with the other Dell leaf at the top of this file.
run_logged "$OMARCHY_INSTALL/hardware/dell-xps13-sidecar-amps.sh"
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-display-backlight.sh"
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-b9406-display.sh"
run_logged "$OMARCHY_INSTALL/hardware/asus/fix-asus-ptl-b9406-touchpad.sh"
@@ -0,0 +1,10 @@
# Enable the temporary sidecar amplifier workaround on the exact Dell XPS 13 model that needs it.
#
# Pacman registers a package even when its post_install scriptlet fails, so the
# apply command runs explicitly here: a failed cleanup or boot-image rebuild has
# to reach the caller rather than hide behind a successfully registered package.
if omarchy-hw-dell-xps13-sidecar-amps; then
omarchy-pkg-add dell-xps13-sidecar-amps &&
sudo dell-xps13-sidecar-amps-apply
fi
+7
View File
@@ -0,0 +1,7 @@
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
+168
View File
@@ -0,0 +1,168 @@
# Chromium-family machine policy is mandatory for every profile. Directories
# stay 0755 root:root; omarchy-theme-set-browser-policy is the privileged
# write for color.json.
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/as-root.sh"
BROWSER_POLICY_MANAGED_DIRS=(
/etc/chromium/policies/managed
/etc/opt/chrome/policies/managed
/etc/opt/edge/policies/managed
/etc/brave/policies/managed
)
# Ancestors of the managed dirs, shortest first. A writable or attacker-owned
# parent can rename the leaf aside; install -d follows a planted symlink.
BROWSER_POLICY_PARENT_DIRS=(
/etc/chromium
/etc/chromium/policies
/etc/opt/chrome
/etc/opt/chrome/policies
/etc/opt/edge
/etc/opt/edge/policies
/etc/brave
/etc/brave/policies
)
BROWSER_POLICY_FIREFOX_DIRS=(
/usr/lib/firefox/distribution
/opt/zen-browser/distribution
)
BROWSER_POLICY_DEFAULT_COLOR="#1c2027"
browser_policy_purge_dir() {
local dir=$1
as_root find "$dir" -mindepth 1 -maxdepth 1 ! -user root -exec rm -rf -- {} +
}
browser_policy_parent_hardened() {
local dir=$1
[[ -d $dir && ! -L $dir ]] || return 1
[[ $(stat -c '%a' "$dir") == "755" ]] || return 1
[[ $(stat -c '%U' "$dir") == "root" ]] || return 1
}
browser_policy_dir_hardened() {
browser_policy_parent_hardened "$1"
}
browser_policy_parents_hardened() {
local dir=$1
local parent
for parent in "${BROWSER_POLICY_PARENT_DIRS[@]}"; do
[[ $dir == "$parent"/* ]] || continue
[[ -e $parent || -L $parent ]] || continue
browser_policy_parent_hardened "$parent" || return 1
done
}
browser_policy_setup_parent() {
local dir=$1
if [[ -L $dir || ( -e $dir && ! -d $dir ) ]]; then
as_root rm -rf -- "$dir"
fi
as_root install -d -m 0755 -o root -g root "$dir"
}
browser_policy_setup_parents_for() {
local dir=$1
local parent
for parent in "${BROWSER_POLICY_PARENT_DIRS[@]}"; do
[[ $dir == "$parent"/* ]] || continue
browser_policy_setup_parent "$parent"
done
}
browser_policy_setup_dir() {
local dir=$1
browser_policy_setup_parents_for "$dir"
browser_policy_setup_parent "$dir"
browser_policy_purge_dir "$dir"
}
# Themes are user-installed. Accept only three 0-255 components.
browser_policy_theme_hex() {
local theme_rgb=$1
if [[ $theme_rgb =~ ^[[:space:]]*([0-9]{1,3})[[:space:]]*,[[:space:]]*([0-9]{1,3})[[:space:]]*,[[:space:]]*([0-9]{1,3})[[:space:]]*$ ]] &&
(( 10#${BASH_REMATCH[1]} < 256 && 10#${BASH_REMATCH[2]} < 256 && 10#${BASH_REMATCH[3]} < 256 )); then
printf '#%02x%02x%02x' "$((10#${BASH_REMATCH[1]}))" "$((10#${BASH_REMATCH[2]}))" "$((10#${BASH_REMATCH[3]}))"
return
fi
printf '%s' "$BROWSER_POLICY_DEFAULT_COLOR"
}
browser_policy_install_color() {
local policy_dir=$1
local hex=$2
local dest=$policy_dir/color.json
local tmp
[[ -d $policy_dir && ! -L $policy_dir ]] || return 0
[[ $hex =~ ^#[0-9a-f]{6}$ ]] || return 1
tmp=$(mktemp) || return 1
printf '{"BrowserThemeColor": "%s", "BrowserColorScheme": "device"}\n' "$hex" >"$tmp"
if [[ -L $dest || -d $dest ]]; then
if ! rm -rf -- "$dest" 2>/dev/null; then
rm -f "$tmp"
return 1
fi
fi
if install -m 0644 -T "$tmp" "$dest" 2>/dev/null; then
rm -f "$tmp"
return 0
fi
rm -f "$tmp"
return 1
}
browser_policy_firefox_policy_file_ok() {
local file=$1
local mode
local group_write
local other_write
[[ -f $file && ! -L $file ]] || return 1
[[ $(stat -c '%U' "$file") == "root" ]] || return 1
mode=$(stat -c '%a' "$file")
group_write=$((8#${mode: -2:1}))
other_write=$((8#${mode: -1}))
(( (group_write & 2) == 0 && (other_write & 2) == 0 ))
}
browser_policy_firefox_hardened() {
local dir=$1
[[ -d $dir && ! -L $dir ]] || return 1
[[ $(stat -c '%a' "$dir") == "755" ]] || return 1
[[ $(stat -c '%U' "$dir") == "root" ]] || return 1
browser_policy_firefox_policy_file_ok "$dir/policies.json"
}
browser_policy_install_firefox_policies() {
local distribution_dir=$1
local policies=${2:-$OMARCHY_PATH/default/firefox/policies.json}
as_root install -m 644 -o root -g root -T "$policies" "$distribution_dir/policies.json"
}
browser_policy_setup_firefox_distribution() {
local distribution_dir=$1
local policies=${2:-$OMARCHY_PATH/default/firefox/policies.json}
browser_policy_setup_parent "$distribution_dir"
browser_policy_purge_dir "$distribution_dir"
browser_policy_install_firefox_policies "$distribution_dir" "$policies"
}
+1
View File
@@ -61,6 +61,7 @@ linux-firmware-marvell
# Dell laptop support packages
dell-xps-touchpad-haptics
dell-xps13-sidecar-amps
# Speaker tunings (LV2 limiter every tuning ends in)
lsp-plugins-lv2
+1 -1
View File
@@ -21,7 +21,7 @@ From the terminal, the same switches are `omarchy toggle <thing>`. Run `omarchy
| Suspend | — | `omarchy toggle suspend` |
| Hybrid GPU | — | `omarchy toggle hybrid gpu` |
The touchpad, touchscreen, and hybrid GPU switches live under _Trigger > Hardware_ (`Super + Ctrl + H`) rather than under Toggle, since they only show up when you actually have that hardware. The touchpad and touchscreen ones survive a Hyprland reload — the disabled state is written back out as a small Lua file that Hyprland sources on startup.
The touchpad, touchscreen, and hybrid GPU switches live under _Trigger > Hardware_ (`Super + Ctrl + H`) rather than under Toggle, since they only show up when you actually have that hardware. The touchpad and touchscreen ones survive a Hyprland reload — the disabled device's name is saved to a small state file that Hyprland reads on startup to disable it again.
The Toggle menu also carries a few things that aren't `omarchy toggle` commands but behave the same: battery percentage in the bar, workspace layout (`Super + L`), window gaps (`Super + Shift + Backspace`), and the 1-window square aspect (`Super + Ctrl + Backspace`).
+2
View File
@@ -39,6 +39,8 @@ Omarchy watches systemd-coredump for process crashes. When something segfaults,
The watching is on by default. Turn it off under _Trigger > Toggle > Crash Capture_ (or with `omarchy toggle crash-capture`) and the notifications stop; `omarchy agent crash <pid>` still works by hand.
Crashes can also be silenced one program at a time, which is what the diagnosis offers you at the end. `omarchy crash mute hyprland` stops the notifications for that program only, `omarchy crash mute hyprland off` brings them back, and `omarchy crash mute` on its own lists what you've muted. It takes the binary's path as happily as its name, so `omarchy crash mute /usr/bin/hyprland` does the same thing. Quote a name with a space in it, as in `omarchy crash mute 'Some App'`. Everything else still notifies, and the muted program still crashes — this hides the reminder, it doesn't fix anything.
### Desktop apps
The _Install > AI_ menu also carries a couple of graphical AI apps: the ChatGPT desktop app, and Grok Bot for chatting with xAI's models.
+1 -1
View File
@@ -2,7 +2,7 @@
### Apple M1/M2 chips
[Asahi Alarm](https://asahi-alarm.org/) is a version of Arch for Apple M1/M2 computers built on top of [Asahi Linux](https://asahilinux.org/). You can get Omarchy running on top of that with some effort. See [the user-driven guide](https://codeberg.org/malik-na/omarchy-mac).
[Asahi Alarm](https://asahi-alarm.org/) is a version of Arch for Apple M1/M2 computers built on top of [Asahi Linux](https://asahilinux.org/). You can get Omarchy running on top of that with some effort. See [the user-driven guide](https://github.com/omarchy-mac/omarchy-mac).
### Apple Virtual Machine
+116
View File
@@ -0,0 +1,116 @@
echo "Take ownership of the FIDO2 authfile so it cannot be rewritten without root"
authfile="/etc/fido2/fido2"
# omarchy-migrate records this migration as complete whenever it exits zero, so
# a line printed here scrolls past once in the update terminal and is never
# shown again. The states below cannot be repaired without deciding what to do
# with a file we do not own, and they are exactly the ones where the authfile
# may already be under someone else's control, so say so where it outlives the
# scrollback as well.
report_unrepairable() {
echo " $1"
echo " $2"
omarchy-notification-send -u critical -g  "FIDO2 authfile needs attention" "$1 $2" || true
}
# Nothing to repair on any machine that never set FIDO2 up, which is almost all
# of them. Checked before any sudo so those machines never see a password
# prompt. -L as well as -e: a dangling symlink is invisible to -e.
if [[ ! -L $authfile && ! -e $authfile ]]; then
# Absence and "cannot look" are the same answer to the tests above. The old
# setup created /etc/fido2 with `sudo mkdir -p`, which took the union of the
# caller's umask and sudoers' 0022, so anyone registering under `umask 077`
# left it mode 0700 with the user-owned authfile still inside. Escalate for
# that case alone -- a machine that never set FIDO2 up has no directory here
# and still reaches exit 0 without a password prompt. Not through a symlink:
# chmod would act on whatever it points at.
authdir=${authfile%/*}
if [[ -L $authdir || ! -d $authdir || -x $authdir ]]; then
exit 0
fi
# Ask root whether a registration is behind it before touching the directory
# itself. An aborted setup that left an empty 0700 directory, or one an
# administrator deliberately keeps private, must not have its mode widened
# and its group and special bits discarded for a repair it does not need.
if ! sudo test -e "$authfile" && ! sudo test -L "$authfile"; then
exit 0
fi
sudo chmod 755 "$authdir"
fi
# The old privileged move could install a symlink here if its fixed staging path
# was redirected. Reported, not repaired: chown follows symlinks and would take
# ownership of the target instead, and removing it would strip sudo and polkit
# from anyone whose only credential is the token.
if [[ -L $authfile ]]; then
report_unrepairable "$authfile is a symlink, not a regular file." \
"Leaving it alone. If you did not create it, remove it and re-run Setup > Security > Fido2."
exit 0
fi
# A directory or a device here is no more ours to rewrite than a symlink is,
# and changing a directory's mode would alter an object we do not own.
if [[ ! -f $authfile ]]; then
report_unrepairable "$authfile is not a regular file." \
"Leaving it alone. Remove it and re-run Setup > Security > Fido2."
exit 0
fi
# Migration state is per-user, so every account re-runs this. The file's own
# ownership is the state check: the second account finds the repair already
# done and exits without escalating.
owner=$(stat -c %U "$authfile" 2>/dev/null) || owner=""
group=$(stat -c %G "$authfile" 2>/dev/null) || group=""
mode=$(stat -c %a "$authfile" 2>/dev/null) || mode=""
if [[ $owner == "root" && $group == "root" && $mode == "644" ]]; then
exit 0
fi
# Setup used to `mv` this in from /tmp, which carried the invoking user's
# ownership into /etc. Root ownership stops that user from rewriting their own
# PAM credential without root. Mode 644 keeps the public credential mapping
# readable when pam_u2f opens an absolute authfile as the authenticating user.
#
# Rename a fresh copy over the path rather than chowning in place. A descriptor
# opened while the file was still the user's own stays writable on that inode
# through any later chmod or chown, since permission is checked at open(2), and
# pam_u2f resolving the path would keep landing on it. Replacing the inode
# leaves that descriptor writing to a file nothing reads.
stage=""
safe_stage_path() {
local candidate=$1
local prefix="$authfile.new."
local suffix
[[ $candidate == "$prefix"* ]] || return 1
suffix=${candidate#"$prefix"}
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
}
cleanup_stage() {
local status=$?
if safe_stage_path "$stage"; then
sudo rm -f -- "$stage" || true
fi
return "$status"
}
trap cleanup_stage EXIT
stage=$(sudo mktemp "$authfile.new.XXXXXX")
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
echo " Could not create a safe staging file beside $authfile."
exit 1
fi
sudo install -T -m 644 -o root -g root "$authfile" "$stage"
sudo mv -Tf "$stage" "$authfile"
stage=""
trap - EXIT
+30
View File
@@ -0,0 +1,30 @@
echo "Stop world-writable Chromium and Firefox policy directories"
source "$OMARCHY_PATH/install/helpers/browser-policy.sh"
repaired=0
for dir in "${BROWSER_POLICY_MANAGED_DIRS[@]}"; do
[[ -d $dir || -L $dir ]] || continue
browser_policy_setup_dir "$dir"
repaired=1
done
# Repainting the policy color is cosmetic and the next theme change redoes it.
# Under bash -euo pipefail a failure here would abort the migration before the
# Firefox directories below are hardened, and the marker would never be written.
if (( repaired )); then
omarchy-theme-set-browser || true
fi
for dir in "${BROWSER_POLICY_FIREFOX_DIRS[@]}"; do
[[ -d $dir || -L $dir ]] || continue
if browser_policy_firefox_hardened "$dir"; then
browser_policy_purge_dir "$dir"
continue
fi
browser_policy_setup_parent "$dir"
browser_policy_purge_dir "$dir"
if ! browser_policy_firefox_policy_file_ok "$dir/policies.json"; then
browser_policy_install_firefox_policies "$dir"
fi
done
+20
View File
@@ -0,0 +1,20 @@
echo "Require signed packages from the Omarchy repository"
# The [omarchy] repo predates the Omarchy packaging key, so existing installs
# carry a SigLevel override that also accepts unsigned packages. Packages are
# signed now, so drop the override and let the repo inherit the global
# SigLevel = Required DatabaseOptional like every other repo. Machine-wide and
# self-detecting, so another user's rerun no-ops.
omarchy_sig_override='SigLevel = Optional TrustAll'
if [[ -f /etc/pacman.conf ]] &&
sed -n '/^\[omarchy\]/,/^\[/p' /etc/pacman.conf | grep -qxF "$omarchy_sig_override"; then
# Requiring signatures with an untrusted packaging key would fail every
# omarchy transaction, including the one that could repair it.
if omarchy-pkg-missing omarchy-keyring ||
! sudo pacman-key --list-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 &>/dev/null; then
omarchy-update-keyring
fi
sudo sed -i "/^\[omarchy\]/,/^\[/{/^$omarchy_sig_override$/d}" /etc/pacman.conf
fi
+39
View File
@@ -0,0 +1,39 @@
echo "Store Hyprland input-device names as data instead of generated Lua"
# omarchy-toggle-input-device used to interpolate hyprctl device names into
# hyprctl eval and a generated Lua file. Those names come from USB descriptors,
# so recover the plain device name as data and delete the generated Lua. A name
# that could have broken out of the old Lua string literal is discarded, not
# trusted. The old script wrote to ~/.local/state regardless of XDG_STATE_HOME.
toggles_dir="$HOME/.local/state/omarchy/toggles/hypr"
reapply=0
for kind in touchpad touchscreen; do
state_file="$toggles_dir/$kind-disabled.lua"
name_file="$toggles_dir/$kind-disabled-name"
[[ -f $state_file ]] || continue
if [[ ! -f $name_file && -r $state_file ]]; then
old=$(<"$state_file")
pattern='^hl\.device\(\{ name = "([^"\\[:cntrl:]]+)", enabled = false \}\)$'
if [[ $old =~ $pattern ]]; then
printf '%s\n' "${BASH_REMATCH[1]}" >"$name_file"
fi
fi
rm -f "$state_file"
if [[ -f $name_file ]]; then
reapply=1
fi
done
# The package hook reloads Hyprland before migrations run, so this session has
# already dropped the disable: the generated Lua is no longer loaded and the
# name file did not exist yet to replace it. Reload once more now that it does,
# or the device the user switched off stays on until their next login.
if (( reapply )); then
hyprctl reload >/dev/null 2>&1 || true
fi
+6
View File
@@ -0,0 +1,6 @@
echo "Enable Dell XPS 13 sidecar speaker amplifiers"
if omarchy-hw-dell-xps13-sidecar-amps; then
source "$OMARCHY_PATH/install/hardware/dell-xps13-sidecar-amps.sh"
omarchy-state set reboot-required
fi
+3
View File
@@ -138,6 +138,7 @@ BorderSurface {
radius: 0
}
contentItem: Text {
textFormat: Text.PlainText
text: root.tooltipText
color: root.tooltipForeground
font.family: root.fontFamily
@@ -158,6 +159,7 @@ BorderSurface {
spacing: Style.spacing.controlGap
Text {
textFormat: Text.PlainText
visible: root.iconText !== ""
text: root.iconText
color: root.selected ? root._selectedColor : root.foreground
@@ -177,6 +179,7 @@ BorderSurface {
}
Text {
textFormat: Text.PlainText
visible: root.text !== ""
text: root.text
color: root.selected ? root._selectedColor : root.foreground
+2
View File
@@ -69,6 +69,7 @@ Item {
Text {
id: messageText
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
@@ -105,6 +106,7 @@ Item {
radius: 0
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
text: modelData
color: destructive ? (selected ? Color.urgent : root.foreground) : (selected ? root.selectedText : root.foreground)
+3
View File
@@ -71,6 +71,7 @@ Item {
spacing: Style.spacing.labelGap
Text {
textFormat: Text.PlainText
visible: root.showLabel && root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
@@ -110,6 +111,7 @@ Item {
}
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter
@@ -214,6 +216,7 @@ Item {
: "transparent"
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
+6
View File
@@ -259,6 +259,7 @@ Item {
spacing: Style.spacing.labelGap
Text {
textFormat: Text.PlainText
visible: root.showLabel && root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
@@ -298,6 +299,7 @@ Item {
}
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter
@@ -451,6 +453,7 @@ Item {
: Border.controlSpec("normal", root.foreground, root.accent)
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
text: root.loadingOptions ? "󰦖" : "󰑐"
color: root.foreground
@@ -486,6 +489,7 @@ Item {
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
visible: resultList.count === 0
text: root.loadingOptions ? "Loading…" : (root.optionsError !== "" ? root.optionsError : root.emptyText)
@@ -581,6 +585,7 @@ Item {
spacing: Style.spacing.xxs
Text {
textFormat: Text.PlainText
text: modelData.label
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
@@ -589,6 +594,7 @@ Item {
width: parent.width
}
Text {
textFormat: Text.PlainText
visible: text !== ""
text: modelData.description
color: Qt.darker(root.foreground, 1.5)
+1
View File
@@ -25,6 +25,7 @@ Column {
spacing: Style.spacing.md
Text {
textFormat: Text.PlainText
visible: root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
+1
View File
@@ -25,6 +25,7 @@ Item {
Text {
id: glyph
textFormat: Text.PlainText
// Keep the shared line box and baseline intact. Correcting only the
// horizontal painted bounds avoids per-glyph vertical drift.
anchors.centerIn: parent
+1
View File
@@ -69,6 +69,7 @@ BorderSurface {
Behavior on color { ColorAnimation { duration: 60 } }
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
text: root.iconText
color: root.enabled
+3
View File
@@ -48,6 +48,7 @@ Item {
width: parent.width
Text {
textFormat: Text.PlainText
visible: root.title !== ""
text: root.title
width: Math.min(implicitWidth, Math.max(0, parent.width - (detailPill.visible ? detailPill.implicitWidth + Style.space(8) : 0)))
@@ -75,6 +76,7 @@ Item {
Text {
id: detailText
textFormat: Text.PlainText
anchors.centerIn: parent
text: root.detail
color: root.dim
@@ -87,6 +89,7 @@ Item {
Text {
id: metaText
textFormat: Text.PlainText
width: parent.width
text: root.meta.toUpperCase()
visible: text !== ""
+4
View File
@@ -11,6 +11,10 @@ Text {
property string fontFamily: Style.font.family
property real fontSize: Style.font.caption
// Callers bind `text` from outside this file, so the default has to be set
// here. AutoText would let a section title that happens to carry a device or
// network name promote itself to rich text.
textFormat: Text.PlainText
color: Qt.darker(foreground, 1.4)
font.family: fontFamily
font.pixelSize: fontSize
+1
View File
@@ -36,6 +36,7 @@ ToolTip {
}
contentItem: Text {
textFormat: Text.PlainText
text: root.text
color: root.panelForeground
font.family: root.fontFamily
+5
View File
@@ -93,6 +93,7 @@ Item {
spacing: Style.spacing.labelGap
Text {
textFormat: Text.PlainText
visible: root.showLabel && root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
@@ -132,6 +133,7 @@ Item {
}
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: chevron.left
anchors.verticalCenter: parent.verticalCenter
@@ -246,6 +248,7 @@ Item {
height: popup.height - searchHeader.height - Style.spacing.xxs - 1
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
visible: resultList.count === 0
text: root.emptyText
@@ -313,6 +316,7 @@ Item {
spacing: Style.spacing.xxs
Text {
textFormat: Text.PlainText
text: root.optionLabel(modelData)
color: index === resultList.currentIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground
font.family: root.fontFamily
@@ -321,6 +325,7 @@ Item {
width: parent.width
}
Text {
textFormat: Text.PlainText
visible: text !== ""
text: root.optionDescription(modelData)
color: Qt.darker(root.foreground, 1.5)
+5
View File
@@ -130,6 +130,7 @@ PanelWindow {
spacing: Style.space(16)
Text {
textFormat: Text.PlainText
visible: root.title !== ""
text: root.title.toUpperCase()
color: root.onScrimDim
@@ -182,6 +183,7 @@ PanelWindow {
}
Text {
textFormat: Text.PlainText
visible: root.failed
text: root.error
color: root.onScrimUrgent
@@ -368,6 +370,7 @@ PanelWindow {
spacing: 0
Text {
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter
// Both branches go through the locale: a reading is a measurement, so
// its separators follow the system's number conventions rather than the
@@ -383,6 +386,7 @@ PanelWindow {
}
Text {
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter
text: root.unit
color: root.onScrimDim
@@ -394,6 +398,7 @@ PanelWindow {
// The 90° gap at the bottom of the scale is where a cluster prints its
// unit; here it names the direction.
Text {
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
text: dial.label
+2
View File
@@ -69,6 +69,7 @@ BorderSurface {
anchors.verticalCenter: parent.verticalCenter
Text {
textFormat: Text.PlainText
text: root.label
color: root.foreground
font.family: root.fontFamily
@@ -79,6 +80,7 @@ BorderSurface {
}
Text {
textFormat: Text.PlainText
visible: root.description !== ""
text: root.description
color: Qt.darker(root.foreground, 1.5)
+1
View File
@@ -74,6 +74,7 @@ Item {
Text {
id: label
textFormat: Text.PlainText
visible: root.labelVisible
anchors.centerIn: parent
text: root.text
+12
View File
@@ -434,6 +434,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
visible: heroMarkImage.status !== Image.Ready
text: button.text
@@ -504,6 +505,7 @@ Panel {
Text {
id: statusText
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -558,6 +560,7 @@ Panel {
Text {
id: balanceValue
textFormat: Text.PlainText
text: root.balance ? root.formatMoney(root.balance.remaining, root.balance.currency) : ""
color: root.balanceAlarming ? root.urgent : root.foreground
font.family: root.fontFamily
@@ -575,6 +578,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
visible: text !== ""
width: parent.width
text: root.balanceDetailText(root.balance)
@@ -680,6 +684,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
visible: text !== ""
width: parent.width
topPadding: Style.space(2)
@@ -710,6 +715,7 @@ Panel {
Text {
id: limitLabel
textFormat: Text.PlainText
// A model-scoped window is titled after its model, and those names run
// long enough to reach the percentage, so the title gives way first.
text: limitRow.window ? limitRow.window.title : ""
@@ -725,6 +731,7 @@ Panel {
Text {
id: limitValue
textFormat: Text.PlainText
text: limitRow.window && limitRow.window.percent >= 0
? Math.round(limitRow.window.percent * 100) + "%"
: "—"
@@ -744,6 +751,7 @@ Panel {
Text {
id: resetText
textFormat: Text.PlainText
width: parent.width
text: {
var remainingMs = root.resetMsFor(limitRow.window)
@@ -798,6 +806,7 @@ Panel {
Text {
id: dayLabel
textFormat: Text.PlainText
text: root.dayLabel(dayRow.day ? dayRow.day.date : "", dayRow.today)
color: dayRow.today ? root.foreground : root.dim
font.family: root.fontFamily
@@ -835,6 +844,7 @@ Panel {
Text {
id: dayValue
textFormat: Text.PlainText
text: usage.formatTokenCount(dayRow.day ? Number(dayRow.day.messageCount || 0) : 0)
color: dayRow.today ? root.foreground : root.dim
font.family: root.fontFamily
@@ -890,6 +900,7 @@ Panel {
Text {
id: modelName
textFormat: Text.PlainText
text: modelRow.row ? modelRow.row.name : ""
color: root.foreground
font.family: root.fontFamily
@@ -904,6 +915,7 @@ Panel {
Text {
id: modelTokens
textFormat: Text.PlainText
text: modelRow.row ? usage.formatTokenCount(modelRow.row.total) : ""
color: root.dim
font.family: root.fontFamily
+1
View File
@@ -1090,6 +1090,7 @@ Item {
Text {
id: tooltipLabel
textFormat: Text.PlainText
anchors.centerIn: parent
text: root.tooltipText
color: Color.tooltip.text
@@ -29,6 +29,7 @@ BarWidget {
Text {
id: labelText
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: parent.width
+4
View File
@@ -467,6 +467,7 @@ BarWidget {
}
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
anchors.left: rowIcon.right
anchors.leftMargin: Style.space(10)
@@ -577,6 +578,7 @@ BarWidget {
}
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: Style.space(28)
@@ -681,6 +683,7 @@ BarWidget {
}
Text {
textFormat: Text.PlainText
visible: !menuRow.modelData.isSeparator && menuRow.modelData.buttonType !== QsMenuButtonType.None
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
@@ -709,6 +712,7 @@ BarWidget {
}
Text {
textFormat: Text.PlainText
visible: !menuRow.modelData.isSeparator
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
+4
View File
@@ -432,6 +432,7 @@ Item {
color: "transparent"
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -500,6 +501,7 @@ Item {
}
Text {
textFormat: Text.PlainText
width: parent.width - (parent.parent.previewImage.length > 0 ? parent.height + parent.spacing : 0)
height: parent.height
text: parent.parent.previewText
@@ -546,6 +548,7 @@ Item {
}
Text {
textFormat: Text.PlainText
visible: parent.activeRow && !parent.activeRow.previewImage
anchors.fill: parent
anchors.leftMargin: root.contentMargin
@@ -593,6 +596,7 @@ Item {
}
Text {
textFormat: Text.PlainText
text: root.history.length === 0 ? "Clipboard is empty" : "No matches for “" + root.filterText + "”"
color: root.foreground
opacity: 0.7
@@ -519,12 +519,14 @@ Item {
width: Style.space(140)
spacing: Style.space(1)
Text {
textFormat: Text.PlainText
text: "Style.font." + modelData.key
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: modelData.size + " px"
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
@@ -534,6 +536,7 @@ Item {
Text {
id: sampleText
textFormat: Text.PlainText
anchors.left: metaCol.right
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -574,6 +577,7 @@ Item {
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: Style.font.family
color: root.foreground
font.family: root.fontFamily
@@ -587,6 +591,7 @@ Item {
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: Style.font.resolvedFamily
color: root.foreground
font.family: root.fontFamily
@@ -600,6 +605,7 @@ Item {
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: Style.font.baseSize + " px"
color: root.foreground
font.family: root.fontFamily
@@ -613,6 +619,7 @@ Item {
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: Style.bar.sizeHorizontal + " px"
color: root.foreground
font.family: root.fontFamily
@@ -626,6 +633,7 @@ Item {
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: Style.bar.sizeVertical + " px"
color: root.foreground
font.family: root.fontFamily
@@ -639,6 +647,7 @@ Item {
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: Style.spacing.scale.toFixed(2)
color: root.foreground
font.family: root.fontFamily
@@ -652,6 +661,7 @@ Item {
font.pixelSize: Style.font.bodySmall
}
Text {
textFormat: Text.PlainText
text: Style.spacing.panelPadding + " px"
color: root.foreground
font.family: root.fontFamily
@@ -818,6 +828,7 @@ Item {
Text {
id: csLabel
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -1273,6 +1284,7 @@ Item {
}
Text {
textFormat: Text.PlainText
text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%"
color: root.foreground
font.family: root.fontFamily
+3
View File
@@ -247,6 +247,7 @@ Item {
color: "transparent"
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -284,6 +285,7 @@ Item {
color: hasCursor ? root.selectedBackground : "transparent"
Text {
textFormat: Text.PlainText
text: parent.emoji
font.family: root.fontFamily
font.pixelSize: Style.font.display
@@ -326,6 +328,7 @@ Item {
}
Text {
textFormat: Text.PlainText
text: "No matches for “" + root.filterText + "”"
color: root.foreground
opacity: 0.7
@@ -545,6 +545,7 @@ Item {
Text {
id: selectedLabel
textFormat: Text.PlainText
visible: root.showLabels
anchors.top: carousel.bottom
anchors.topMargin: Style.space(16)
@@ -561,6 +562,7 @@ Item {
}
Text {
textFormat: Text.PlainText
visible: root.filterable && root.filterText
anchors.top: selectedLabel.bottom
anchors.topMargin: Style.space(8)
+1
View File
@@ -184,6 +184,7 @@ Item {
}
Text {
textFormat: Text.PlainText
anchors.fill: passwordInput
text: root.authenticatingPassword ? "Checking…" : (root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText)
visible: passwordInput.text.length === 0
+7
View File
@@ -1199,6 +1199,7 @@ Item {
color: "transparent"
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -1287,6 +1288,7 @@ Item {
Text {
id: iconText
textFormat: Text.PlainText
visible: row.hasIcon && !row.isApp
text: row.icon
color: row.hasCursor ? root.selectedText : root.foreground
@@ -1328,6 +1330,7 @@ Item {
Text {
id: labelText
textFormat: Text.PlainText
width: parent.width
text: row.label
color: row.hasCursor ? root.selectedText : root.foreground
@@ -1338,6 +1341,7 @@ Item {
}
Text {
textFormat: Text.PlainText
width: parent.width
text: row.detail
visible: (root.filterText || row.kind === "dmenu") && row.detail.length > 0
@@ -1358,6 +1362,7 @@ Item {
spacing: 0
Text {
textFormat: Text.PlainText
visible: false
text: row.childCount
color: root.foreground
@@ -1368,6 +1373,7 @@ Item {
}
Text {
textFormat: Text.PlainText
text: row.kind === "menu" || row.kind === "link" ? "" : ""
color: row.hasCursor ? root.selectedText : root.foreground
opacity: row.kind === "menu" || row.kind === "link" ? 0.36 : 0
@@ -1452,6 +1458,7 @@ Item {
}
Text {
textFormat: Text.PlainText
text: root.filterText ? "No matches for “" + root.filterText + "”" : "Nothing here yet"
color: root.foreground
opacity: 0.7
@@ -5,8 +5,92 @@ function isChromiumDerived(app, appIcon) {
source.indexOf("opera") >= 0
}
// True when a `<...>` run is an image tag, so the name is read the way Qt's
// parser reads it: after the `<`, the leading run of letters and digits.
//
// Skip everything up to that run rather than matching the separator, because
// there is no JavaScript expression for what Qt skips. QQuickStyledText calls
// skipSpace(), which is QChar::isSpace(), and that set is not `\s`: Qt counts
// U+0085 NEL and `\s` does not, while `\s` counts U+FEFF and Qt does not. A
// name read with `\s` therefore misses a tag written as `<`, U+0085, `img`:
// Qt skips the NEL, reads `img` and issues the GET, while the regex finds no
// name at all and the tag is kept. Measured against Qt 6.11.2.
//
// Over-skipping is the safe direction. It can only classify more runs as
// images, and dropping a run never manufactures a tag: a dropped run joins two
// stretches of text that each contain no `<`.
function isImageTag(tag) {
var name = /^<[^A-Za-z0-9]*([A-Za-z0-9]+)/.exec(tag)
return !!name && name[1].toLowerCase() === "img"
}
// The body renders as StyledText so notifications can use the markup the
// body-markup capability advertises (see Service.qml). StyledText honours
// <img src>, and a remote src makes the shell issue an unauthenticated GET
// with no user action, so image tags go before the renderer sees them.
//
// Work in whole tags, never in substrings of one. A `<` opens a tag that runs
// to the next `>`, nested `<` and all, and only a tag whose own name is `img`
// is dropped.
//
// That is the conservative bound, not Qt's exact one: Qt lets a `>` inside a
// quoted attribute value pass without closing the tag, so a Qt tag can be
// longer than the run taken here. Do not "correct" this to match Qt. Taking
// the shorter run only ever splits one Qt tag into several, and a split can
// only expose an `<img` to be dropped, never hide one — whereas honouring
// quotes would let `<b title="a>b"><img src="http://host/x.png">` through.
//
// Deleting a substring is what makes a naive `/<img[^>]*>/g` unsafe. Given
//
// <im<img src="http://a/decoy.png">g src="http://a/beacon.png">
//
// Qt reads ONE malformed tag named `im` and renders nothing, but removing the
// inner match closes the surviving halves up into `<img src=".../beacon.png">`
// — a live tag the input never contained. The stripper would be manufacturing
// the very thing it exists to remove.
//
// Because every `<` opens a tag, the text between tags never contains one, so
// dropping a tag cannot splice its neighbours into a new one. That makes a
// single pass sufficient, with no re-scanning and no input bound to police.
function stripImageTags(text) {
var out = ""
var i = 0
while (i < text.length) {
var open = text.indexOf("<", i)
if (open === -1) {
out += text.slice(i)
break
}
out += text.slice(i, open)
// An unterminated tag at the end of the string still reaches the renderer,
// which closes it itself, so treat the remainder as one tag.
var close = text.indexOf(">", open)
var tag = close === -1 ? text.slice(open) : text.slice(open, close + 1)
if (!isImageTag(tag)) out += tag
i = close === -1 ? text.length : close + 1
}
return out
}
// What the card renders, and the last thing to touch the string before Qt parses
// it. The newline rewrite belongs here rather than in the card because it inserts
// `<br/>` into text stripImageTags chose to KEEP, and a kept tag may hold a `<` of
// its own: `<x`, newline, `<img src="http://…">` is one tag named `x` to both the
// stripper and Qt, until the rewrite splits it into `<x<br/>` and a live image tag
// the input never contained. Measured against Qt 6.11.2 — the rewritten form
// fetches, the original does not. So strip again after, and what Qt parses is what
// was checked last.
function styledBody(body, app, appIcon) {
return stripImageTags(sanitizeBody(body, app, appIcon).replace(/\r\n|\r|\n/g, "<br/>"))
}
function sanitizeBody(body, app, appIcon) {
var text = String(body || "").replace(/<img[^>]*>/gi, "")
var text = stripImageTags(String(body || ""))
if (!isChromiumDerived(app, appIcon)) return text
return text
@@ -366,6 +450,7 @@ if (typeof module !== "undefined") {
module.exports = {
isChromiumDerived: isChromiumDerived,
sanitizeBody: sanitizeBody,
styledBody: styledBody,
summaryStartsWithGlyph: summaryStartsWithGlyph,
shouldBypassDnd: shouldBypassDnd,
isEphemeralApp: isEphemeralApp,
@@ -44,7 +44,7 @@ BorderSurface {
readonly property bool singleLineToast: sanitizedBody.length === 0
readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph
readonly property string sanitizedBody: sanitizeBody(body)
readonly property string styledBody: sanitizedBody.replace(/\r\n|\r|\n/g, "<br/>")
readonly property string styledBody: NotificationLogic.styledBody(body, app, appIcon)
readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4)
readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15)
@@ -133,6 +133,7 @@ BorderSurface {
// Glyph fallback (Nerd Font character) when no image icon is
// available. Used by omarchy-notification-send's `-g` flag.
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
visible: root.hasGlyph && smallIconImage.status !== Image.Ready
text: root.glyph
@@ -143,6 +144,7 @@ BorderSurface {
}
Text {
textFormat: Text.PlainText
Layout.alignment: Qt.AlignVCenter
visible: root.compactGlyph
text: root.glyph
@@ -159,6 +161,11 @@ BorderSurface {
spacing: Style.space(2)
Text {
// The spec defines the summary as a single line of plain text, so
// AutoText could only ever promote a hostile string to rich text.
// The body below is StyledText on purpose — see Service.qml's
// bodyMarkupSupported — and is stripped in NotificationLogic.
textFormat: Text.PlainText
Layout.fillWidth: true
visible: root.summary.length > 0
text: root.summary
+2
View File
@@ -159,6 +159,7 @@ Item {
width: root.iconWidth
height: parent.height
Text {
textFormat: Text.PlainText
// Sit the glyph's ink flush in the column, centered when the
// column is wider than this particular glyph.
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
@@ -186,6 +187,7 @@ Item {
}
}
Text {
textFormat: Text.PlainText
visible: root.message !== ""
width: root.hasProgress ? root.valueWidth : root.messageWidth
// The readout hugs the card edge so a short percentage doesn't leave
+11
View File
@@ -711,6 +711,7 @@ Panel {
// Status only — the switch owns muting, mouse and keyboard alike.
Text {
id: heroIcon
textFormat: Text.PlainText
text: root.outputIcon()
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -761,6 +762,7 @@ Panel {
Text {
id: heroLabel
textFormat: Text.PlainText
text: root.outputVolumeName(
outputSlider.dragging ? outputSlider.liveValue : root.outputVolume,
root.outputMuted
@@ -800,6 +802,7 @@ Panel {
Text {
id: outputPercent
textFormat: Text.PlainText
text: Math.round((outputSlider.dragging ? outputSlider.liveValue : root.outputVolume) * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
@@ -886,6 +889,7 @@ Panel {
Text {
id: microphonePercent
textFormat: Text.PlainText
text: Math.round((inputSlider.dragging ? inputSlider.liveValue : root.inputVolume) * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
@@ -1030,6 +1034,7 @@ Panel {
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
text: root.sinkGlyph(sinkRow.node)
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1040,6 +1045,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
text: root.nodeLabel(sinkRow.node)
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1089,6 +1095,7 @@ Panel {
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
text: root.sourceGlyph(sourceRow.node)
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1099,6 +1106,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
text: root.nodeLabel(sourceRow.node)
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1159,6 +1167,7 @@ Panel {
Text {
id: streamMuteIcon
textFormat: Text.PlainText
text: streamRow.streamMuted ? "󰝟" : "󰕾"
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1179,6 +1188,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
text: root.streamLabel(streamRow.node)
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1191,6 +1201,7 @@ Panel {
Text {
id: streamPct
textFormat: Text.PlainText
text: Math.round(streamRow.streamVolume * 100) + "%"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
+6
View File
@@ -698,6 +698,7 @@ Panel {
// Status only the switch owns toggling, mouse and keyboard alike.
Text {
id: heroIcon
textFormat: Text.PlainText
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: root.icon
@@ -748,6 +749,7 @@ Panel {
Text {
id: heroStatus
textFormat: Text.PlainText
text: root.heroStatusText.toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
@@ -863,6 +865,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
visible: root.connectedDevices.length === 0 && root.scrollRows.length === 0
text: !root.adapter ? "No Bluetooth adapter"
: !root.adapter.enabled ? "Turn Bluetooth on to scan"
@@ -971,6 +974,7 @@ Panel {
Text {
id: deviceIcon
textFormat: Text.PlainText
text: row.isConnected ? "󰂱" : "󰂯"
color: row.statusColor
font.family: root.bar.fontFamily
@@ -989,6 +993,7 @@ Panel {
anchors.verticalCenter: parent.verticalCenter
Text {
textFormat: Text.PlainText
text: root.deviceLabel(row.dev) || "Device"
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -997,6 +1002,7 @@ Panel {
width: parent.width
}
Text {
textFormat: Text.PlainText
visible: row.statusText !== ""
text: row.statusText
color: row.statusColor
+8
View File
@@ -311,6 +311,7 @@ Panel {
Text {
id: heroDate
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: Qt.formatDate(root.today, "MMMM d")
color: heroMouse.containsMouse
@@ -413,6 +414,7 @@ Panel {
Text {
id: yearLabel
textFormat: Text.PlainText
visible: !root.editingLife
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
@@ -425,6 +427,7 @@ Panel {
Text {
id: yearPercent
textFormat: Text.PlainText
visible: !root.editingLife
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -485,6 +488,7 @@ Panel {
Text {
id: lifePercent
textFormat: Text.PlainText
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: root.lifeDonePercent + "%"
@@ -608,6 +612,7 @@ Panel {
model: root.weekdays
Text {
textFormat: Text.PlainText
required property var modelData
width: root.cellWidth
height: Style.space(16)
@@ -631,6 +636,7 @@ Panel {
spacing: root.cellSpacing
Text {
textFormat: Text.PlainText
width: root.weekColumnWidth
height: root.cellHeight
horizontalAlignment: Text.AlignHCenter
@@ -662,6 +668,7 @@ Panel {
border.color: Style.normalBorderFor(root.contentForeground, Color.accent)
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
text: modelData.day
color: modelData.inMonth
@@ -707,6 +714,7 @@ Panel {
Text {
id: monthLabel
textFormat: Text.PlainText
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
// Fixed width so the chevrons hold still between a
+8
View File
@@ -281,6 +281,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
visible: dropbox.actionStatus !== "" || dropbox.lastError !== ""
width: parent.width
text: dropbox.actionStatus !== "" ? dropbox.actionStatus : dropbox.lastError
@@ -421,6 +422,7 @@ Panel {
spacing: Style.space(1)
Text {
textFormat: Text.PlainText
Layout.fillWidth: true
text: dropbox.installed ? "Login to Dropbox" : "Dropbox CLI is not installed"
color: root.foreground
@@ -430,6 +432,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
Layout.fillWidth: true
text: dropbox.installed ? "Start the authentication flow" : "Install Dropbox from the service menu"
color: root.dim
@@ -478,6 +481,7 @@ Panel {
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
text: Model.fileGlyph(fileRow.fileName)
color: root.foreground
font.family: root.fontFamily
@@ -491,6 +495,7 @@ Panel {
spacing: Style.space(1)
Text {
textFormat: Text.PlainText
Layout.fillWidth: true
text: fileRow.fileName
color: root.foreground
@@ -500,6 +505,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
Layout.fillWidth: true
text: Model.fileMeta(fileRow.file)
color: root.dim
@@ -524,6 +530,7 @@ Panel {
}
component InfoLabel: Text {
textFormat: Text.PlainText
color: root.foreground
opacity: 0.6
font.family: root.fontFamily
@@ -531,6 +538,7 @@ Panel {
}
component InfoValue: Text {
textFormat: Text.PlainText
color: root.foreground
font.family: root.fontFamily
font.pixelSize: Style.font.bodySmall
+7
View File
@@ -531,6 +531,7 @@ Panel {
Text {
id: heroIcon
textFormat: Text.PlainText
text: root.displays.length > 1 ? "󰍺" : "󰍹"
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -559,6 +560,7 @@ Panel {
Text {
id: heroLabel
textFormat: Text.PlainText
text: {
if (root.brightnessAvailable) {
return root.brightnessName(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent).toUpperCase()
@@ -602,6 +604,7 @@ Panel {
Text {
id: brightnessPercent
textFormat: Text.PlainText
text: Math.round(brightnessSlider.dragging ? brightnessSlider.liveValue : root.brightnessPercent) + "%"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
@@ -674,6 +677,7 @@ Panel {
Text {
id: textSizePx
textFormat: Text.PlainText
text: (textSizeSlider.dragging
? root.textSizeStops[Math.round(textSizeSlider.liveValue)]
: root.displayedTextPx()) + "px"
@@ -747,6 +751,7 @@ Panel {
// focused one.
Text {
id: scaleMonitor
textFormat: Text.PlainText
text: root.focusedMonitor
// Only worth naming when more than one display is in play.
visible: root.focusedMonitor !== "" && root.enabledDisplayCount > 1
@@ -887,6 +892,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
text: monitorRow.display.name + (monitorRow.display.focused ? " · focused" : "")
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -897,6 +903,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
text: monitorRow.display.enabled ? "󰄬" : ""
color: root.bar.foreground
font.family: root.bar.fontFamily
+10
View File
@@ -1090,6 +1090,7 @@ Panel {
// Status only the switch owns toggling, mouse and keyboard alike.
Text {
id: heroIcon
textFormat: Text.PlainText
text: root.icon
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1170,6 +1171,7 @@ Panel {
// rather than in a pill, which crowded the on/off switch.
Text {
id: heroSsid
textFormat: Text.PlainText
width: parent.width
readonly property string title: {
@@ -1189,6 +1191,7 @@ Panel {
Text {
id: heroMeta
textFormat: Text.PlainText
width: parent.width
text: {
if (root.info.type === "wifi") {
@@ -1711,6 +1714,7 @@ Panel {
Text {
id: networkIcon
textFormat: Text.PlainText
text: row.net ? root.wifiIconFor(row.net.signal) : ""
color: row.statusColor
font.family: root.bar.fontFamily
@@ -1732,6 +1736,7 @@ Panel {
Text {
id: lockIndicator
textFormat: Text.PlainText
visible: row.requiresCredentials || row.forgetVisible
width: parent.width
anchors.verticalCenter: parent.verticalCenter
@@ -1779,6 +1784,7 @@ Panel {
anchors.verticalCenter: parent.verticalCenter
Text {
textFormat: Text.PlainText
text: row.net ? (row.net.ssid || "Hidden") : ""
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -1787,6 +1793,7 @@ Panel {
width: parent.width
}
Text {
textFormat: Text.PlainText
// Signal strength is conveyed by the wifi-bars icon and the
// right-edge glyph/buttons carry protection or forget affordances,
// so the second line only carries action status (Connecting,
@@ -1893,6 +1900,7 @@ Panel {
radius: Style.cornerRadius
Text {
textFormat: Text.PlainText
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
@@ -1946,6 +1954,7 @@ Panel {
}
component InfoLabel: Text {
textFormat: Text.PlainText
color: root.bar.foreground
opacity: 0.6
font.family: root.bar.fontFamily
@@ -1953,6 +1962,7 @@ Panel {
}
component InfoValue: Text {
textFormat: Text.PlainText
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
+5
View File
@@ -325,6 +325,7 @@ Panel {
Text {
id: heroIcon
textFormat: Text.PlainText
text: root.batteryIcon()
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -356,6 +357,7 @@ Panel {
Text {
id: heroStatus
textFormat: Text.PlainText
text: root.heroStatusText.toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
@@ -369,6 +371,7 @@ Panel {
Text {
id: heroPercent
textFormat: Text.PlainText
text: root.batteryInfo.percentage || "—"
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -517,6 +520,7 @@ Panel {
}
component InfoLabel: Text {
textFormat: Text.PlainText
color: root.bar.foreground
opacity: 0.6
font.family: root.bar.fontFamily
@@ -524,6 +528,7 @@ Panel {
}
component InfoValue: Text {
textFormat: Text.PlainText
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.bodySmall
+10
View File
@@ -498,6 +498,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
visible: tailscale.actionStatus !== "" || tailscale.lastError !== ""
width: parent.width
text: tailscale.actionStatus !== "" ? tailscale.actionStatus : tailscale.lastError
@@ -841,6 +842,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
text: accountRow.accountText
color: root.foreground
font.family: root.fontFamily
@@ -933,6 +935,7 @@ Panel {
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
text: tailscale.osIcon(peer ? peer.OS : "")
color: root.foreground
font.family: root.fontFamily
@@ -946,6 +949,7 @@ Panel {
spacing: Style.space(1)
Text {
textFormat: Text.PlainText
Layout.fillWidth: true
text: peerRow.peerName
color: root.foreground
@@ -955,6 +959,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
Layout.fillWidth: true
text: {
var parts = []
@@ -1087,6 +1092,7 @@ Panel {
spacing: Style.space(10)
Text {
textFormat: Text.PlainText
Layout.fillWidth: true
text: copyChoice.label
color: root.foreground
@@ -1134,6 +1140,7 @@ Panel {
Text {
id: exitNodeGlyph
textFormat: Text.PlainText
text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "󰖂" : "󱇢")
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim
font.family: root.fontFamily
@@ -1154,6 +1161,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
text: exitNodeRow.peerName
color: root.foreground
font.family: root.fontFamily
@@ -1224,6 +1232,7 @@ Panel {
spacing: Style.space(1)
Text {
textFormat: Text.PlainText
width: parent.width
text: regionRow.regionName
color: root.foreground
@@ -1234,6 +1243,7 @@ Panel {
}
Text {
textFormat: Text.PlainText
width: parent.width
text: regionRow.regionDetail
visible: text !== ""
+14
View File
@@ -531,6 +531,7 @@ Panel {
Text {
id: heroIcon
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 5
text: root.label || "—"
@@ -547,6 +548,7 @@ Panel {
Text {
id: tempBig
textFormat: Text.PlainText
text: root.reportTempNum || "—"
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -556,6 +558,7 @@ Panel {
font.bold: true
}
Text {
textFormat: Text.PlainText
text: root.current ? root.tempUnit : ""
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -593,6 +596,7 @@ Panel {
anchors.verticalCenter: parent.verticalCenter
}
Text {
textFormat: Text.PlainText
text: (root.reportLocation || "").toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
@@ -643,6 +647,7 @@ Panel {
color: !root.savingLocation && clearLocationArea.containsMouse ? Style.hoverFillFor(root.bar.foreground, Color.accent) : "transparent"
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
text: root.savingLocation ? "󰦖" : "✕"
font.family: root.bar.fontFamily
@@ -683,6 +688,7 @@ Panel {
font.letterSpacing: 1
}
Text {
textFormat: Text.PlainText
text: root.reportFeels
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -700,6 +706,7 @@ Panel {
font.letterSpacing: 1
}
Text {
textFormat: Text.PlainText
text: root.reportWind
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -717,6 +724,7 @@ Panel {
font.letterSpacing: 1
}
Text {
textFormat: Text.PlainText
text: root.reportHumidity
color: root.bar.foreground
font.family: root.bar.fontFamily
@@ -752,12 +760,14 @@ Panel {
spacing: Style.space(8)
Text {
textFormat: Text.PlainText
text: modelData.name
color: index === root.suggestionIndex ? Style.hoverStateColor(root.bar.foreground, Color.accent) : root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.body
}
Text {
textFormat: Text.PlainText
visible: text !== ""
text: modelData.description
color: Qt.darker(root.bar.foreground, 1.5)
@@ -817,6 +827,7 @@ Panel {
spacing: Style.space(10)
Text {
textFormat: Text.PlainText
anchors.verticalCenter: parent.verticalCenter
text: root.dayIcon(modelData)
color: root.bar.foreground
@@ -829,6 +840,7 @@ Panel {
spacing: Style.space(2)
Text {
textFormat: Text.PlainText
text: root.dayName(modelData.date).toUpperCase()
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
@@ -840,12 +852,14 @@ Panel {
spacing: Style.space(6)
Text {
textFormat: Text.PlainText
text: root.bareTempForDay(modelData, "max")
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: Style.font.body
}
Text {
textFormat: Text.PlainText
text: root.bareTempForDay(modelData, "min")
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
+3
View File
@@ -257,6 +257,7 @@ Item {
spacing: Style.space(16)
Text {
textFormat: Text.PlainText
text: (root.ssid || "Wi-Fi").toUpperCase()
color: root.onScrimDim
font.family: root.fontFamily
@@ -318,6 +319,7 @@ Item {
}
Text {
textFormat: Text.PlainText
visible: root.error !== ""
text: root.error
color: root.onScrimUrgent
@@ -340,6 +342,7 @@ Item {
}
Text {
textFormat: Text.PlainText
visible: root.showingQr && root.secured
text: root.passwordError !== "" ? root.passwordError
: root.passwordVisible ? root.password
+2
View File
@@ -332,6 +332,7 @@ Item {
}
Text {
textFormat: Text.PlainText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -374,6 +375,7 @@ Item {
Text {
id: justificationText
textFormat: Text.PlainText
anchors.fill: parent
anchors.leftMargin: Style.space(12)
anchors.rightMargin: Style.space(12)

Some files were not shown because too many files have changed in this diff Show More