Compare commits

...
Author SHA1 Message Date
ZacharyZhang-NY f3b236ae82 Pick the matching Noto CJK variant for language-tagged text
The generic assigns swap sans-serif, serif and monospace for fonts
with no CJK glyphs, so tagged Chinese text ends up on whatever Noto
CJK variant the charset scan finds first, usually the Japanese one.
Fold the many Chinese tags down to one per variant and prepend the
matching family ahead of the assigns.
2026-10-12 00:11:53 -04:00
bjarneo 9480d1b6a9 Merge pull request #15065 from bjarneo/fix/bar-icon-consistency
Normalize bar icon size and alignment
2026-10-11 22:07:57 +02:00
Bjarne Oeverli 6575f06d53 Correct custom bar icon bounds 2026-10-11 22:02:37 +02:00
Bjarne Oeverli 201de1c0d6 Normalize bar icon size and alignment 2026-10-11 21:39:17 +02:00
David Heinemeier HanssonandClaude Opus 5.5 e71a816e90 Cache workspace layouts instead of running a script on every switch (#15028)
* Cache workspace layouts instead of running a script on every switch

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

* Test the workspace layout cache by running its logic, not matching its source

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

* Keep the fake layout read running while its exit handler runs

Addresses Codex review: clearing running first let an undeferred, broken retry pass the cache tests.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:03:30 +09:00
David Heinemeier HanssonandClaude Opus 5.5 1b1cc5ecb5 Track the power profile through Quickshell instead of polling busctl (#15027)
* Track the power profile through Quickshell instead of polling busctl

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

* Recover the power profile when the daemon was down at shell start

Addresses Codex review: Quickshell never attaches its PowerProfiles listener if the daemon failed at startup. One-off reads on startup, power source changes and an IPC refresh from omarchy-powerprofiles-set cover that, still with no timer.

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

* Don't let a stale power profile read win over a change signal or drop a refresh

Addresses Codex re-review: a busctl read in flight could overwrite a newer PowerProfiles signal, and refreshes requested while a read ran were dropped.

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

* Test power profile transitions and stale reads as behavior, not source text

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:01:49 +09:00
David Heinemeier HanssonandClaude Opus 5.5 3c1f7b780f Refresh the passwordless sudo indicator on change instead of polling (#15026)
* Refresh the passwordless sudo indicator on change instead of polling

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

* Refresh the sudo indicator after failed changes and on a wall-clock deadline

Addresses Codex review: a rolled-back renewal skipped the refresh, the monotonic user timer paused through suspend, and default timer accuracy allowed a minute's delay.

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

* Test the sudo indicator refreshes by running the grant flows

Replace source-string checks with runs of timed, renewed, failed, permanent and disable flows against the fixture, asserting the recorded omarchy-shell refresh and wall-clock systemd-run --user timer.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:01:45 +09:00
David Heinemeier HanssonandClaude Opus 5.5 8bd1b7c045 Tell agents to react to events instead of polling (#15029)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:42:26 +09:00
David Heinemeier Hansson c4803d7d74 Update comment 2026-10-11 22:33:14 +09:00
David Heinemeier HanssonandClaude Opus 5.5 5f25ef1da8 Let Neovim's current line and status line show the terminal background on existing installs (#15024)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:30:32 +09:00
David Heinemeier HanssonandClaude Opus 5.5 88de9ecc6c Let herdr's tab bar show the terminal background, so it turns glassy too (#15022)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 22:25:16 +09:00
David Heinemeier HanssonandClaude Opus 5.5 1ac31e55ae Never leave the desktop without a wallpaper after a theme intro (#15000)
A theme switch that plays an intro gave the shell the new still only when
the intro failed. The shell kept the old theme's path, which the theme swap
deletes, and loaded it again when OWE handed the desktop back, leaving it
empty. The new still is now handed over either way.

The shell also re-reads the background link whenever OWE hands the desktop
back or a wallpaper fails to load, falling back to the theme's first
background when the link itself is broken.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:42:00 +09:00
David Heinemeier HanssonandClaude Opus 5.5 754d2d04b3 Only recover a session lock no other locker may hold (#15020)
Hyprland lets any client take over a held lock, so a second shell started
beside the desktop judged the real shell's live lock stranded, took it, and
then exited, leaving the session behind the failsafe while the real shell
still reported itself secure. Recovery now waits while any other Quickshell
or hyprlock runs, and gives up if one never exits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:26:49 +09:00
David Heinemeier HanssonandClaude Opus 5.5 a98e55eb91 Pick window transparency from Appearance > Window Transparency, and a Glassy bar background (#14991)
* Pick window transparency from Appearance > Window Transparency

Transparent, the default, keeps today's windows. Opaque turns the windows
Omarchy makes transparent solid. Glassy is the frosted glass Starship
(#14726): its blur behind every see-through window and shell surface, and a
see-through, blurred background in the theme's terminal files, since foot
turns compositor blur down and a Hyprland opacity rule fades text.

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

* Leave window transparency to Appearance when animations are off

Turning animations off no longer makes windows opaque. VM installs start
with opaque windows instead, and a migration keeps them opaque for anyone
who already has animations off.

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

* Add a Glassy bar background, under Menu Bar > Bar Background

Menu Bar > Bar Background picks Opaque Bar, Glassy Bar or Clear Bar, and
Indicators becomes Bar Indicators to match. Glassy keeps the theme
background faintly and Hyprland blurs what is behind it, with the same blur
as Glassy windows.

omarchy bar background opaque|glassy|none|cycle sets it and prints it when
given nothing. omarchy bar transparent true|false|toggle stays for
compatibility, as none and opaque. Double-clicking the bar cycles through
the three backgrounds the same way, from clear to glassy to opaque.

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

* Stop when a saved corner choice can't be removed

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

* Make btop follow glassy windows

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:21:00 +09:00
David Heinemeier HanssonandClaude Opus 5.5 be7c7bf378 Replace Xournal++ with Signed for signing PDFs (#14999)
Signed, Omacom's PDF signing app, takes Xournal++'s place in the base
install and the preinstall set, so Remove and Install Preinstalls cover
it too. A migration installs it on existing machines, unless their owner
has removed the preinstalls; Xournal++ stays on machines that already
have it. The Filling out PDFs chapter now walks through Signed, the GUIs
chapter introduces it, the setup tip points to it, and the unused
Xournal++ config is gone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 10:57:16 +02:00
David Heinemeier HanssonandClaude Opus 5.5 4df545905d Pick window corners from Appearance > Window Corners (#14990)
* Expect Appearance before Toggle under Trigger

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

* Read the default workspace layout once for all the menu rows that ask

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

* Pick window corners from Appearance > Window Corners

Theme Corners leaves them to each theme. Sharp and Rounded override every
theme through a Hyprland toggle, with Rounded using Solitude's corners.

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

* Keep the corner choice when saving a new one fails, and test the load order

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:06:54 +09:00
David Heinemeier Hansson 3882c27cfd Better ordering 2026-10-11 16:11:18 +09:00
David Heinemeier HanssonandClaude Opus 5.5 5ec8bd300b Pick the bar clock format from Appearance > Menu Bar > Indicators > Clock
Every preset the clock cycles through gets its own row, labeled with how
it reads, and the current one is checked. omarchy-bar gains get <id> <key>
and a bare position to read the settings the rows compare against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:10:33 +09:00
David Heinemeier HanssonandClaude Opus 5.5 eb1a34ecd0 Move Battery Percentage under Appearance > Menu Bar > Indicators
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:03:50 +09:00
David Heinemeier HanssonandClaude Opus 5.5 b6ceb8f4d6 Use the menu's animations icon in the toggle notifications
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:00:47 +09:00
David Heinemeier HanssonandClaude Opus 5.5 bef30c1c50 Move the Menu Bar from Style to Appearance, with Hide and Show (#14982)
* Match the menu test to the rearranged Trigger menu

Trigger now leads with Toggle and Appearance, and Appearance's entries,
now including Herdr Theme Sync, are in their new order.

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

* Move the Menu Bar from Style to Appearance, with Hide and Show

Trigger > Appearance > Menu Bar now holds the bar's Position and
Transparency from Style > Menu Bar, and offers Hide while the bar shows
and Show while it is hidden, instead of a bare toggle.

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

* Name the Menu Bar entries after the bar, and split transparency

Hide Bar, Show Bar, Position Bar, Top Bar and the rest read on their own
when searched. Transparency becomes Transparent Bar while the bar is
opaque and Opaque Bar while it is transparent.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:59:25 +09:00
David Heinemeier HanssonandClaude Opus 5.5 18e059d9a4 Show the remote session indicator only during a remote session (#14981)
Hovering the indicators revealed it dimmed alongside the ones that can be
switched on, though there is nothing to do with it there. An indicator can
now opt out of that reveal, and remote session does.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:58:19 +09:00
David Heinemeier Hansson cf473b5c5d Rearrange menu items a bit 2026-10-11 15:50:04 +09:00
David Heinemeier HanssonandClaude Opus 5.5 d1a614ce66 Decide lock requests from the current lock state (#14978)
The locked binding once read true after an unlock, though the request,
the session lock, and its secure state were all false. A lock before
suspend then took the session for locked and did nothing, and the
machine slept unlocked. Lock requests, the status reply, and stranded
lock recovery now read those three values directly.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:37:45 +02:00
1498718969 Add a floating mode with titlebars to the workspace layout cycle (#14482)
* Let the workspace layout test's Lua assertions fail the suite

Lua 5.5's standalone interpreter exits 0 when the script that raised the error came in on stdin, and exits 1 for the same error in a script file. The block here was piped in through a heredoc, so every assertion in it reported as passing however wrong the value was: changing the expected layout to one the toggle never writes still left the file green.

* Add a floating mode to the workspace layout cycle

Super + L cycled a workspace between dwindle and scrolling. It now carries on into floating, where every window on the workspace floats.

Floating cannot be a third `layout` on the workspace rule. Hyprland has no layout by that name and accepts one anyway: `hl.workspace_rule({ layout = "floating" })` returns ok and silently leaves the workspace on dwindle, exactly as a misspelt name does. So the mode floats the windows themselves, through the same `float` dispatcher SUPER + T uses.

Ending the mode has to tile the windows the mode floated and only those, so each one is tagged with `omarchy-mode-floated`. A window already floating when the mode reaches it -- an app with a float rule of its own, a dialog, one popped out with SUPER + T -- is left unclaimed and is still floating when the mode ends. The tag lives on the window rather than in a table here: it dies with the window, follows it between workspaces, and survives the config reload that plugging in a monitor performs, none of which a table on this side would do. A pinned window is left alone entirely, because tiling one unpins it as well.

Windows arrive on a workspace three ways and a window rule only covers one of them, so the mode watches events: window.open for what opens there, window.move_to_workspace for what is carried in, and the same event for what is carried back out, which gives up the floating the mode gave it rather than staying floating on a workspace that tiles. Every tiled window is claimed before any of them is floated, because floating one member of a group floats the rest, and claiming as it went would find those already floating and leave them stranded.

The mode is read back from the saved file rather than from the compositor. Hyprland reports a workspace's tiled layout and floating is not one, so a floating workspace still answers dwindle or scrolling; trusting that answer would skip a step of the cycle. A file saved before floating existed names a layout rather than a mode and falls through to what Hyprland reports, so the cycle keeps working and the next press rewrites it.

Two faults in the old script had to go first, because saving the mode makes both persistent. Named workspaces report a negative id, which passed the `^-?[0-9]+$` guard, and Hyprland reads a leading "-" as a selector relative to the current workspace: pressing Super + L on a named workspace reconfigured workspace 1 instead, and would now save a file that did it again at every login. And the saved state was written under $HOME/.local/state while the Hyprland side reads XDG_STATE_HOME, which also had to reach package.path: the loader lists that directory but loads the files by module name, so a state home anywhere else listed the saved layouts and then loaded same-named ones from the home directory.

Saving is checked too. The mode reaching the compositor and the mode reaching disk are separate things that can fail separately, and a save that failed silently would leave the next press reading a file that never got written, cycling between two modes and never reaching the third.

The guard against named workspaces stops new files being written, but an earlier version already wrote them, and they go on being loaded at every login. A migration removes the saved layouts named for a negative id.

The keyword fallback is gone. `hyprctl keyword` refuses a Lua configuration outright -- "keyword can't work with non-legacy parsers. Use eval." -- and exits 0 while doing so, so as a fallback it neither worked nor reported that it had not. A failed apply now says so and saves nothing, rather than announcing a layout the screen does not show and restoring it at the next login.

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

* Add titlebars, edge snapping, and a window Shelf to floating workspaces

Floating workspaces become a traditional desktop that works with every
theme. Windows get themed titlebars from the omarchy-hyprland-titlebars
plugin, which is loaded only while something floats. Dragging a titlebar
to the left or right edge snaps the window to that half, and dragging it
to the top or double-clicking maximizes it. The window floating mode
places is sized, centered, and cascaded, and floating workspaces focus
on click.

Super + M, or the minus button, sets a window aside on the Shelf, which
works from any workspace and restores the window onto the current one
in that workspace's layout. Float All Workspaces makes floating the
default while Super + L still sets a single workspace's layout.

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

* Bring resize on border to tiling too

* Fix floating mode cleanup, fullscreen windows, and failed Shelf notes

Turning Float All Workspaces off with no workspace layouts left behind
started nothing to give the floated windows back. A window fullscreen
when its workspace began floating now floats once it leaves fullscreen.
A window whose Shelf note cannot be saved stays where it is, and the
titlebars read the theme from where omarchy-theme-set writes it.

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

* Place the Shelf on the bar once, instead of on every minimize

Putting it back on each minimize kept a Shelf the user moved where they
put it, but re-added one they had removed. A migration places it once for
existing bars; Super + Alt + M opens the Shelf either way.

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

* Raise titlebars off the window background

A titlebar in the theme's background color ran into the window under it.
It takes the theme's lighter background, the raised surface every theme
defines, and the selection color on themes that make the two the same.

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

* Drop minimizing and edge snapping from floating workspaces

A floating workspace is now titlebars with maximize and close, and nothing
else. The Shelf (its bar widget, panel, commands, notes, Super + M and
Super + Alt + M) goes with the minimize button, and the titlebar plugin's
edge snapping is left off, along with the shell's snap preview.

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

* Stop describing the titlebar package as snapping

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

* Name web app windows in their launchers

Chromium names a web app window after its URL, and titles it with the page
title or just the domain. StartupWMClass ties the window to its launcher,
so the titlebar plugin can show the launcher's Name instead: WhatsApp
rather than web.whatsapp.com. New web apps, the shipped ones, and existing
launchers (through a migration) all carry it.

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

* Drop the titlebar package comment from the base list

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

* Show the workspace layout in the bar and cycle it on click

The focused workspace's mark is a square for dwindle, a triangle for
scrolling, and a circle for floating, and clicking it cycles the layout
as Super + L does. Both layout toggles ask the bar to read the new mode.

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

* Show scrolling as a circle and floating as a triangle

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

* Move the look of the desktop to Trigger > Appearance

Menu Bar, Battery Percentage, Workspace Layout, Window Gaps, Animations,
and 1-Window Ratio change how the desktop looks rather than switching a
behavior, so they leave Trigger > Toggle for their own menu.

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

* Choose a default workspace layout instead of floating everything

Trigger > Appearance > Workspace Layout sets the layout of every workspace
without one of its own: Tiling, Scrolling, or Floating. A tiled default
is Hyprland's own layout, and a floating one is the floating mode. It
replaces Float All Workspaces, which also overrode and set aside the
layouts workspaces had been given with Super + L.

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

* Title the workspace layout menu Default Workspace Layout

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

* Use the animation icon for Appearance > Animations

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

* Open clipboard history from Trigger

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

* Leave edge resizing to looknfeel on floating workspaces

looknfeel now resizes from borders everywhere, so floating workspaces
only need to switch pointer focus to click.

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

* Convert saved workspace layout rules to modes

Released versions saved a workspace's layout as a plain workspace rule,
which names no mode, so a floating default would override a workspace
set to scrolling, and the bar and Super + L would misread it. A migration
rewrites those files as modes.

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

* Cycle workspace layouts backwards with Super + Alt + L

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

* Test the Trigger > Appearance split and Clipboard History placement

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

* Decode escaped web app URLs before naming their windows

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

* Describe the workspace widget's layout mark and click

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

* Let a workspace cycled onto the default follow it again

Once Super + L gave a workspace a layout of its own, it kept it for good,
so a later change of the default never reached it. Cycling onto the
default layout now forgets the workspace's own instead of saving it.
Without a saved default, the default is Hyprland's own layout, which
the user's looknfeel may have set.

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

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 08:20:45 +02:00
David Heinemeier HanssonandClaude Opus 5.5 c09ddcff90 Move the look of the desktop to Trigger > Appearance (#14973)
* Move the look of the desktop to Trigger > Appearance

Menu Bar, Battery Percentage, Workspace Layout, Window Gaps, Animations,
and 1-Window Ratio change how the desktop looks rather than switching a
behavior, so they leave Trigger > Toggle for their own menu.

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

* Use the animation icon for Appearance > Animations

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

* Open clipboard history from Trigger

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

* Test the Trigger > Appearance split and Clipboard History placement

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:17:33 +02:00
David Heinemeier HanssonandClaude Opus 5.5 dc50bbe574 Switch out of an input method and back with a lone Shift (#14975)
* Switch out of an input method and back with a lone Shift

Fcitx's alternate trigger only acts while an engine like Pinyin is or was
active, so Shift + letter and plain Latin typing are unaffected. It needs
Shift releases intact, which the both-Shifts Caps Lock option prevented.

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

* Follow XDG_CONFIG_HOME in the Shift migration and test it

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:15:03 +09:00
35a55fd236 Remove redundant agent guidance in user customization skill (#5925)
* Remove skill body trigger conditions; bolster frontmatter content.

* Keep window behavior and screen recording in the skill's triggers

The removed "When This Skill MUST Be Used" section listed both, and the description that replaces it had dropped them, so a request to record the screen or change how windows behave no longer matched anything the agent reads before choosing the skill.

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

---------

Co-authored-by: omarchybot <omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex Medium <noreply@openai.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
2026-10-11 07:45:22 +02:00
David Heinemeier HanssonandClaude Opus 5.5 10f1cbf7cf Install clang with the Rust dev environment instead of for everyone (#14967)
Nothing in the base install builds with clang: Qt apps and Ruby's native
gems compile with gcc, and Ruby comes prebuilt from mise. Rust crates that
use bindgen, including rb-sys gems, need libclang, so the Rust dev
environment installs it. llvm was unused even there.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:37:23 +09:00
d74d6ea249 Build the calendar month grid in UTC (#10476)
* Build the calendar month grid in UTC

The grid walked a cursor at local midnight and stepped it with setDate(getDate() + 1). Chile starts DST at 24:00 on the first Saturday of September, so local midnight of 6 September 2026 does not exist, and Qt's V4 engine resolves a missing local time backward -- onto 5 September 23:00, the same calendar date, which the next iteration reads again. September 2026 draws 30 31 1 2 3 4 5 and then 5 6 7 8 9 10 11, and every later date sits one weekday early. Cuba, Lebanon, the Azores and Egypt change over at 00:00 too and break the same way in their own transition month; where the gap lands on a leading padding cell instead, the whole six-week grid shifts by a day with no duplicate in it.

Node resolves the identical gap forward, so the suite has never seen this: running the shipped monthGrid under Node across all 598 tzdata zones for 2020-2035 reports nothing, while the same code under the QML engine breaks 402 grids. A sequence of civil dates never needed a wall clock, so it walks UTC, where every day is 24 hours long, and the test pins that by building the grid with a Date whose local constructor and accessors throw.

stepMonth reads a month back off the same kind of date. No zone puts a midnight gap on a first of the month, so it is not broken today, but it moves with the grid rather than waiting for a tzdata release to make it a second bug.

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

* Name the calendar heading's month from local noon

The heading built a local midnight on the first of the month, and that midnight does not always exist: Cuba started DST at 00:00 on 1 April 2012, so in Havana the QML engine resolves it back onto 31 March and the heading reads March 2012 above an April grid. Noon exists on every first of the month there has been, and keeping it local is what makes Qt.formatDate print the right month in every zone.

The Apia case comes from #13186: Samoa skipped 30 December 2011 entirely, so unlike a one-hour midnight gap, Node's V8 cannot resolve its way past it, and the test fails on the old grid under Node instead of only in the shell.

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

---------

Co-authored-by: Codex XHigh <noreply@openai.com>
Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:14:23 +02:00
David Heinemeier HanssonandClaude Opus 5.5 3609968a42 Stop shipping OBS Studio and Kdenlive to new installs (#14962)
Monologue records and Omacut trims, which covers what most people used
them for. Existing installs keep both; there is no migration, and the
preinstall removal no longer treats them as Omarchy's to uninstall.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:57:28 +09:00
David Heinemeier Hansson c1096aa36f Refresh all default theme previews (#14944) 2026-10-11 12:50:06 +09:00
ccf263611d Correct omarchy-pkg-add AUR claim in AGENTS.md (#13895)
* Correct omarchy-pkg-add AUR claim in AGENTS.md

* Say omarchy-pkg-add installs from the pacman repos, not the official ones

It runs plain pacman -S, which also reaches the [omarchy] repo every shipped pacman.conf enables, so "official repos" would send an agent to the AUR helper for Omarchy's own packages.

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

---------

Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Codex Medium <noreply@openai.com>
2026-10-11 05:33:32 +02:00
stephenlentonandClaude Sonnet 5 4d3a85969a Clarify that omarchy-toggle-bar's on/off tracks the bar-off flag (#11803)
on hides the bar and off shows it, matching the literal flag name
rather than an inferred "bar visibility on" reading. The behavior is
correct and depended on by omarchy-toggle-fullscreen-desktop; this
just makes the --help summary say so.


Claude-Session: https://claude.ai/code/session_01YQK8wd7WU8qEVnXVnNmcSW

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-11 05:32:58 +02:00
a3d4bea9d0 docs(agents): add plugin UI safety baseline (#7792)
* docs(agents): add plugin UI safety baseline

Document safe rendering and validation of dynamic plugin data after a marketplace review surfaced a QML AutoText boundary.

Assisted-by: Codex/GPT-5

* docs(agents): allow consented background refreshes

Clarify that plugin networking may refresh periodically when disclosed and user-controlled.

Assisted-by: Codex/GPT-5

* docs(agents): keep plugin guide label concise

Leave the safety baseline discoverable within the plugin guide without expanding its topic index label.

Assisted-by: Codex/GPT-5

* docs(agents): point plugin authors at their own markup helper

Omarchy ships no markup-neutralizing helper, and this skill is for installed systems, where the parent SKILL.md forbids editing the packaged shell components the old line suggested extending.

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

---------

Co-authored-by: Omarchybot <317366263+omarchybot@users.noreply.github.com>
Co-authored-by: Codex Medium <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 05:17:02 +02:00
kevin koontz 9d47dd1497 Point the plugin manual at plugins.omarchy.org (#10499)
The marketplace moved to the official host on 30 Aug 2026. omarchyplugins.com redirects, but the published manual should name the current URL.
2026-10-11 05:00:53 +02:00
Vtorrealba 77b13fd026 Fix typo on 04_navigation.md (#7463) 2026-10-11 04:59:07 +02:00
Ed Schofield 9476e47a10 docs: add 2020 iMac models to the T2 Mac list (#10783) 2026-10-11 04:57:41 +02:00
Spencer Bull 9830142834 Merge pull request #14935 from omacom/startup-cursor-recovery
Recover the startup cursor across Hyprland reloads
2026-10-10 20:59:21 -05:00
Spencer BullandCodex XHigh 0bcb6a2559 Keep the default cursor alias on ordinary boots
Omarchy never sets XCURSOR_THEME, so asking GSettings on every cold boot replaced the "default" alias with whatever GSettings held, which is the schema default Adwaita when nothing was ever set. That overrode a theme chosen through ~/.icons/default and exported the substitute to every application. The saved theme and GSettings are now consulted only when recovering from the blank startup theme or an interrupted reveal.

Co-Authored-By: Codex XHigh <codex@openai.com>
2026-10-10 20:55:29 -05:00
David Heinemeier Hansson 04b374a989 Add Starship theme preview (#14942) 2026-10-11 09:15:47 +09:00
David Heinemeier HanssonandClaude Opus 5.5 1b5222be74 Return Caps Lock to normal and make Shift + Caps Lock the compose key (#14939)
* Return Caps Lock to normal and make Shift + Caps Lock the compose key

Caps Lock as compose, with both Shifts as Caps Lock, confused too many
people. Stock XKB has no option for compose on the shifted Caps key, so
Omarchy defines one in /etc/xkb, which libxkbcommon reads before the
system rules. The explicit actions keep the Shift level from toggling
Caps Lock. Lone Shift is no longer part of Caps Lock, which leaves it
free for input methods.

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

* Require xkbcli so the keymap check cannot silently skip

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 09:06:59 +09:00
David Heinemeier HanssonandClaude Opus 5.5 0b9dace018 Unify input methods with installer keyboard selection (#14433)
* Unify input methods with the installer keyboard selection

* Preserve inherited input keys and later keyboard changes

* Exercise input engines across desktop toolkits

* Show input modes in the keyboard indicator

* Keep Pinyin setup prompts visible

* Remember input selections before an app gains focus

* Remove the Japanese input welcome notification

* Show explicit choices on notification cards

* Suppress the Cloudpinyin first-use prompt

* Show the input switching shortcut in keyboard hints

* Verify input setup and composition stay quiet

* Use Super+I for input switching independently of Fcitx keys

* Restore linked input settings and attribute test notifications to Fcitx

* Correct the input setup shortcut hint

* Keep input tooltips to the name and switching shortcut

* Place the input language selector with the right bar widgets

* Show input switching only for multiple layouts or methods

* Unify input method and keyboard layout switching

* Select keyboard layouts and input methods in the Typing menu

* Apply typing selections immediately from the menu

* Use installer keyboard choices and keep typing selections accurate

* Queue typing changes and wait for saved selections

* Keep queued typing selections when reopening the menu

* Cycle keyboard layouts alongside composition engines

* Avoid shadowing Python typing and retain pending mixed input selections

* Keep input clicks working through controller and compositor failures

* Move the keyboard layout widget to the right side of existing bars

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

* Map keyboard choices to the layouts the installer sets

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

* Keep the switching order when changing input methods

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

* Set input method fonts only after the selection saves

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

* Restore confirmed picker checks when an immediate change fails

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

* Route Super+I through the shell's input reader so unfocused choices stick

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

* Restore the input group when the font preference cannot be written

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

* Drop the azerty conversion now that the installer no longer offers it

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

* Apply the owner's keyboard layout once before persisting the input choice

A quattro merge kept a second apply_keyboard after the retry loop, which
re-ran loadkeys and firstboot, ignored their failure, and overwrote the
XKB layout setup supplies for keymaps kbd-model-map lacks.

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

* Give the Lao keyboard choice a Lao desktop layout

The row used la-latin1, kbd's Latin American Spanish keymap, so Lao
installs typed Spanish. There is no Lao console keymap; like Korean, the
console stays US and the desktop gets la, which Omarchy keeps behind US
as a non-Latin layout.

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

* Load engines installed after Fcitx started when they are selected

The Typing menu refused an engine whose package an update had installed,
since Fcitx discovers engines only at startup. Restart input once and wait
for the engine; name the package when it really is not installed.

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

* Cycle input methods back with Super+Shift+I

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

* Drop notification action buttons now that the Pinyin prompt is suppressed

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

* Cycle input only through the shell's reader

Without the shell there is no bar or menu either, so the one-shot cycling
path only duplicated the reader's logic.

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

* Add engines from the command line through the Typing menu's path

omarchy setup input <engine> had its own restart, append, select and
live-rollback code in configure.py, parallel to the menu's. It now
installs the packages and appends through typing_setup, leaving
configure.py with install-time seeding and update defaults.

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

* Keep Ctrl+Space out of Fcitx's toggles instead of probing for it

The terminal prefix bind read the Fcitx config and profile on every press
to avoid fighting a Ctrl+Space toggle. configure --defaults now removes
that key from every trigger list, so the bind only checks for a terminal.

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

* Run the input-method scripts with the system Python

A bare python resolves through PATH, so a mise, pyenv or venv interpreter
could run them without the system's PyGObject.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:16:30 +09:00
Spencer BullandGPT-6.1-Sol XHigh f5170dd070 Recover the startup cursor across Hyprland reloads
Hyprland discards Lua globals and timers during reload, so the pending startup reveal must survive outside Lua state. Preserve the normal cursor theme for this compositor session, recover orphaned blank settings, and repair activation environments before future applications inherit them.

🤖 Generated by GPT-6.1-Sol High in Codex through T3 Code. Reviewed by GPT-6.1-Sol XHigh in Codex through T3 Code.

Co-Authored-By: GPT-6.1-Sol XHigh <codex@openai.com>
2026-10-10 17:17:28 -05:00
223 changed files with 7775 additions and 771 deletions

No files matched your search

+5 -1
View File
@@ -104,7 +104,7 @@ Use these instead of raw shell commands:
- `omarchy-cmd-missing` / `omarchy-cmd-present` - check for commands - `omarchy-cmd-missing` / `omarchy-cmd-present` - check for commands
- `omarchy-pkg-missing` / `omarchy-pkg-present` - check for packages (don't use these if you can just use `omarchy-pkg-add`/`omarchy-pkg-drop`) - `omarchy-pkg-missing` / `omarchy-pkg-present` - check for packages (don't use these if you can just use `omarchy-pkg-add`/`omarchy-pkg-drop`)
- `omarchy-pkg-add` - install packages (handles both pacman and AUR) - `omarchy-pkg-add` - install packages from the configured pacman repos, Omarchy's included; use `omarchy-pkg-aur-add` for AUR packages
- `omarchy-pkg-drop` - remove packages; use this instead of raw `pacman -R*` - `omarchy-pkg-drop` - remove packages; use this instead of raw `pacman -R*`
- `omarchy-notification-send` - send desktop notifications; do not call `notify-send` directly - `omarchy-notification-send` - send desktop notifications; do not call `notify-send` directly
- `omarchy-hw-asus-rog` - detect ASUS ROG hardware (and similar `hw-*` commands) - `omarchy-hw-asus-rog` - detect ASUS ROG hardware (and similar `hw-*` commands)
@@ -113,6 +113,10 @@ Commands installed by Omarchy's default package set are runtime invariants. Invo
Exceptions are allowed for migration and package-helper scripts where the helper may not be available yet, where the helper itself is being implemented, or where direct package-manager behavior is required. Exceptions are allowed for migration and package-helper scripts where the helper may not be available yet, where the helper itself is being implemented, or where direct package-manager behavior is required.
# Background Work
- Long-running code (the shell, daemons, watchers) reacts to events instead of polling with a timer. Polling is the last resort, for state with no change signal; [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) lists the event sources and the push-refresh pattern.
# Menu # Menu
- The menu definition lives in `default/omarchy/omarchy-menu.jsonc`; - The menu definition lives in `default/omarchy/omarchy-menu.jsonc`;
+11
View File
@@ -38,6 +38,17 @@ Run `omarchy-restart-shell` after making changes to QML files.
bar widgets register one target each — `omarchy.indicators`, bar widgets register one target each — `omarchy.indicators`,
`omarchy.system-update`, `omarchy.clock`. There is no `bar` target. `omarchy.system-update`, `omarchy.clock`. There is no `bar` target.
## Keeping state current without polling
The shell runs all day on battery, and a widget is built once per monitor, so a `Timer` that starts a `Process` costs that process times the number of monitors, every tick, forever. Don't poll for state that only changes through known events. Pick the first of these that fits:
- A native Quickshell service that signals on change: `UPower` and `PowerProfiles` (`Quickshell.Services.UPower`), `Hyprland` and each workspace's `lastIpcObject`, `Pipewire`, `Mpris`, `SystemTray`, `Notifications`.
- An event stream the shell already runs, such as the Hyprland IPC events or the long-running `pactl subscribe` in `Commons/UntypedInput.qml`. Extend it rather than starting a second one.
- A file watch: `FileView` with `watchChanges`, or the directory watch in `plugins/bar/Bar.qml`, when the state lives in a file the user can read.
- A push from the command that changes the state. If only Omarchy commands change it, have them call `omarchy-shell -q <target> refresh` after the change, the way `omarchy-reminder` and `omarchy-sudo-passwordless` do. When the change happens later, without a command running (an expiry, say), schedule the refresh with a transient `systemd-run --user --on-active=…` timer. A root-owned timer cannot reach the user's shell.
Polling is the last resort, kept for state with no change signal at all (a remote API, say). Then gate the timer on the panel being open where you can, pick the longest interval the UI can tolerate, and put the poll in one shared service rather than in a per-monitor widget. Never poll a privileged probe: every `sudo -n` can write to the auth log.
## Editing widget files with glyphs ## Editing widget files with glyphs
Widget files in `shell/plugins/bar/widgets/` contain Nerd Font glyphs as raw Widget files in `shell/plugins/bar/widgets/` contain Nerd Font glyphs as raw
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Basecamp Name=Basecamp
Exec=omarchy-launch-webapp https://launchpad.37signals.com Exec=omarchy-launch-webapp https://launchpad.37signals.com
StartupWMClass=chrome-launchpad.37signals.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=basecamp Icon=basecamp
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Discord Name=Discord
Exec=omarchy-launch-webapp https://discord.com/channels/@me Exec=omarchy-launch-webapp https://discord.com/channels/@me
StartupWMClass=chrome-discord.com__channels_@me-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=omarchy-discord Icon=omarchy-discord
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Contacts Name=Google Contacts
Exec=omarchy-launch-webapp https://contacts.google.com/ Exec=omarchy-launch-webapp https://contacts.google.com/
StartupWMClass=chrome-contacts.google.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-contacts Icon=google-contacts
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Maps Name=Google Maps
Exec=omarchy-launch-webapp https://maps.google.com Exec=omarchy-launch-webapp https://maps.google.com
StartupWMClass=chrome-maps.google.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-maps Icon=google-maps
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Messages Name=Google Messages
Exec=omarchy-launch-webapp https://messages.google.com/web/conversations Exec=omarchy-launch-webapp https://messages.google.com/web/conversations
StartupWMClass=chrome-messages.google.com__web_conversations-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-messages Icon=google-messages
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=Google Photos Name=Google Photos
Exec=omarchy-launch-webapp https://photos.google.com/ Exec=omarchy-launch-webapp https://photos.google.com/
StartupWMClass=chrome-photos.google.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=google-photos Icon=google-photos
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=WhatsApp Name=WhatsApp
Exec=omarchy-launch-webapp https://web.whatsapp.com/ Exec=omarchy-launch-webapp https://web.whatsapp.com/
StartupWMClass=chrome-web.whatsapp.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=whatsapp Icon=whatsapp
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=X Name=X
Exec=omarchy-launch-webapp https://x.com/ Exec=omarchy-launch-webapp https://x.com/
StartupWMClass=chrome-x.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=x Icon=x
+1
View File
@@ -2,6 +2,7 @@
Version=1.0 Version=1.0
Name=YouTube Name=YouTube
Exec=omarchy-launch-webapp https://youtube.com/ Exec=omarchy-launch-webapp https://youtube.com/
StartupWMClass=chrome-youtube.com__-Default
Terminal=false Terminal=false
Type=Application Type=Application
Icon=youtube Icon=youtube
+104 -10
View File
@@ -2,7 +2,7 @@
# omarchy:summary=Configure the bar and its widget layout # omarchy:summary=Configure the bar and its widget layout
# omarchy:group=bar # omarchy:group=bar
# omarchy:args=use <id> | reset | defaults | position <top|bottom|left|right> | transparent <true|false|toggle> | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement] # omarchy:args=use <id> | reset | defaults | position [top|bottom|left|right] | background [opaque|glassy|none|cycle] | put <id> [placement] | move <id> [placement] | set <id> <key> <value> [--json] [placement] | get <id> <key>
# omarchy:examples=omarchy bar use local.neon-bar | omarchy bar put omarchy.keyboard-layout --after omarchy.clock | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar set omarchy.clock format HH:mm # omarchy:examples=omarchy bar use local.neon-bar | omarchy bar put omarchy.keyboard-layout --after omarchy.clock | omarchy bar move omarchy.clock --section center --index 0 | omarchy bar set omarchy.clock format HH:mm
set -euo pipefail set -euo pipefail
@@ -16,13 +16,19 @@ Usage: omarchy bar <command> [args...]
use <id> Use a bar option as the active bar use <id> Use a bar option as the active bar
reset Return to the built-in Omarchy bar reset Return to the built-in Omarchy bar
defaults Restore the default bar and service widgets defaults Restore the default bar and service widgets
position <top|bottom|left|right> Bar position position [top|bottom|left|right] Bar position; prints the current one
transparent <true|false|toggle> Bar transparency when given none
background [opaque|glassy|none|cycle]
Bar background: glassy shows what is
behind the bar blurred, cycle steps
none, glassy, opaque; prints the
current one when given none
put <id> [placement] Put a widget on the bar, leaving one put <id> [placement] Put a widget on the bar, leaving one
that is already there where it is that is already there where it is
move <id> [placement] Move a widget within or between sections move <id> [placement] Move a widget within or between sections
set <id> <key> <value> [--json] [placement] set <id> <key> <value> [--json] [placement]
Set a per-widget option Set a per-widget option
get <id> <key> Print a per-widget option
Placement: Placement:
--section <left|center|right> Target section --section <left|center|right> Target section
@@ -192,30 +198,92 @@ cmd_defaults() {
" \ " \
--slurpfile defaults "$DEFAULTS_FILE" \ --slurpfile defaults "$DEFAULTS_FILE" \
--argjson widgets "$optional_widgets" --argjson widgets "$optional_widgets"
sync_glass || fail "could not change the blur behind the bar"
echo "Restored the default Omarchy bar" echo "Restored the default Omarchy bar"
} }
cmd_position() { cmd_position() {
local position="${1:-}" if (( $# == 0 )); then
[[ -n $position ]] || fail "position is required" jq -r '.bar.position // "top"' "$(source_file)"
return
fi
local position="$1"
(( $# == 1 )) || fail "position takes a single value" (( $# == 1 )) || fail "position takes a single value"
[[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right" [[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right"
commit "$NORMALIZE | .bar.position = \$position" --arg position "$position" commit "$NORMALIZE | .bar.position = \$position" --arg position "$position"
echo "Bar position set to $position" echo "Bar position set to $position"
} }
# The bar background is opaque, glassy, or none. A shell.json from before it
# had one says transparent instead, which is none, and setting a background
# drops it.
current_background() {
jq -r '.bar.background // (if .bar.transparent == true then "none" else "opaque" end)' "$(source_file)"
}
cmd_background() {
if (( $# == 0 )); then
current_background
return
fi
local background="$1"
(( $# == 1 )) || fail "background takes a single value"
[[ $background =~ ^(opaque|glassy|none|cycle)$ ]] || fail "background must be opaque, glassy, none, or cycle"
if [[ $background == "cycle" ]]; then
# Each step changes the bar by one degree: clear, then glassy, then opaque.
case $(current_background) in
none) background="glassy" ;;
glassy) background="opaque" ;;
*) background="none" ;;
esac
fi
local previous
previous=$(current_background)
commit "$NORMALIZE | .bar.background = \$background | del(.bar.transparent)" --arg background "$background"
if ! sync_glass; then
commit "$NORMALIZE | .bar.background = \$background | del(.bar.transparent)" --arg background "$previous"
fail "could not change the blur behind the bar"
fi
echo "Bar background set to $background"
}
# The older on/off form of the background, kept for scripts and dotfiles:
# transparent is none, and not transparent is opaque.
cmd_transparent() { cmd_transparent() {
local transparent="${1:-}" local transparent="${1:-}"
[[ -n $transparent ]] || fail "transparent is required" [[ -n $transparent ]] || fail "transparent is required"
(( $# == 1 )) || fail "transparent takes a single value" (( $# == 1 )) || fail "transparent takes a single value"
[[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle" [[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle"
if [[ $transparent == "toggle" ]]; then if [[ $transparent == "toggle" ]]; then
commit "$NORMALIZE | .bar.transparent = (.bar.transparent != true)" [[ $(current_background) == "none" ]] && transparent="false" || transparent="true"
echo "Bar transparency toggled"
else
commit "$NORMALIZE | .bar.transparent = \$transparent" --argjson transparent "$transparent"
echo "Bar transparency set to $transparent"
fi fi
if [[ $transparent == "true" ]]; then
cmd_background none
else
cmd_background opaque
fi
}
# Hyprland blurs behind the bar only while the bar-glassy toggle is on, so it
# follows the bar background, reloading Hyprland only when it changes. The
# toggle sits where Hyprland reads it, under XDG_STATE_HOME.
sync_glass() {
local dir="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
local flag="$dir/bar-glassy.lua"
if [[ $(current_background) == "glassy" ]]; then
[[ ! -f $flag ]] || return 0
mkdir -p "$dir" && cp "$OMARCHY_PATH/default/hypr/toggles/bar-glassy.lua" "$flag" || return 1
else
[[ -f $flag ]] || return 0
rm -f "$flag" || return 1
fi
hyprctl reload >/dev/null 2>&1 || true
} }
bar_widget_default_section() { bar_widget_default_section() {
@@ -363,6 +431,21 @@ cmd_set() {
echo "Set $key on $id" echo "Set $key on $id"
} }
# Prints the option from the first instance of the widget on the bar, raw for a
# string and as JSON otherwise, and fails when no instance sets it.
cmd_get() {
local id="${1:-}"
local key="${2:-}"
[[ -n $id ]] || fail "get requires a widget id"
[[ -n $key ]] || fail "get requires a setting key"
(( $# == 2 )) || fail "get takes a widget id and a setting key"
jq -e -r --arg id "$id" --arg key "$key" '
first(.bar.layout[]?[]? | select(.id == $id) | .[$key] | select(. != null))
| if type == "string" then . else tojson end
' "$(source_file)" 2>/dev/null || exit 1
}
# --------------------------------------------------------------------- dispatch # --------------------------------------------------------------------- dispatch
command="${1:-}" command="${1:-}"
@@ -385,6 +468,14 @@ case "$command" in
transparent) transparent)
cmd_transparent "$@" cmd_transparent "$@"
;; ;;
background)
cmd_background "$@"
;;
# Internal: the shell runs this when it loads a bar background, so one set
# by editing shell.json gets its blur too.
sync-glass)
sync_glass || fail "could not change the blur behind the bar"
;;
put) put)
cmd_put "$@" cmd_put "$@"
;; ;;
@@ -394,6 +485,9 @@ case "$command" in
set) set)
cmd_set "$@" cmd_set "$@"
;; ;;
get)
cmd_get "$@"
;;
-h | --help | help | "") -h | --help | help | "")
usage usage
;; ;;
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# omarchy:summary=Show or set window corners: theme, sharp, or rounded
# omarchy:args=[theme|sharp|rounded]
# omarchy:examples=omarchy hyprland corners | omarchy hyprland corners rounded
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
# Sharp and rounded are toggle files loaded after every theme, so they override
# the corners of whichever theme is current. Theme removes both.
if (( $# == 0 )); then
if [[ -f $TOGGLES_DIR/corners-sharp.lua ]]; then
echo "sharp"
elif [[ -f $TOGGLES_DIR/corners-rounded.lua ]]; then
echo "rounded"
else
echo "theme"
fi
exit 0
fi
case $1 in
theme) LABEL="Corners follow the theme" ;;
sharp) LABEL="Sharp corners for all themes" ;;
rounded) LABEL="Rounded corners for all themes" ;;
*)
echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2
exit 1
;;
esac
# Save the new choice before dropping the old one, so a failed save keeps it.
if [[ $1 != "theme" ]]; then
mkdir -p "$TOGGLES_DIR" && cp "$OMARCHY_PATH/default/hypr/toggles/corners-$1.lua" "$TOGGLES_DIR/corners-$1.lua" || exit 1
fi
for mode in sharp rounded; do
[[ $mode == "$1" ]] || rm -f "$TOGGLES_DIR/corners-$mode.lua" || exit 1
done
hyprctl reload >/dev/null || exit 1
omarchy-notification-send -g 󰘇 "$LABEL"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# omarchy:summary=Returns true when the compositor holds a session lock no other locker is alive to own
# omarchy:args=<own-pid>
# omarchy:hidden=true
# Hyprland lets any client take over a held lock (misc:allow_session_lock_restore),
# so a shell that recovers a lock it merely sees can take the live lock screen of
# another shell, and strand it when it exits. A lock is stranded only while no
# other Quickshell or hyprlock is running; until then the answer is undetermined,
# so the caller keeps asking until that locker exits or it gives up.
#
# Exits 0 stranded, 1 unlocked, 2 undetermined. The caller passes its own pid,
# as a crash re-execs the shell in place.
omarchy-hyprland-session-locked || exit
if pgrep -u "$UID" -x 'quickshell|qs|hyprlock' | grep -qvxF "$1"; then
exit 2
fi
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
# omarchy:summary=Show or set how see-through windows are: opaque, transparent, or glassy
# omarchy:args=[opaque|transparent|glassy]
# omarchy:examples=omarchy hyprland window transparency | omarchy hyprland window transparency glassy
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
# Opaque and glassy are toggle files loaded after every theme: opaque makes the
# windows Omarchy makes transparent solid, and glassy blurs what is behind any
# see-through window and gives terminals a see-through background. Transparent,
# the default, removes both.
current_mode() {
if [[ -f $TOGGLES_DIR/window-opaque.lua ]]; then
echo "opaque"
elif [[ -f $TOGGLES_DIR/window-glassy.lua ]]; then
echo "glassy"
else
echo "transparent"
fi
}
# Save the new choice before dropping the old one, so a failed save keeps it.
save_mode() {
if [[ $1 != "transparent" ]]; then
mkdir -p "$TOGGLES_DIR" && cp "$OMARCHY_PATH/default/hypr/toggles/window-$1.lua" "$TOGGLES_DIR/window-$1.lua" || return 1
fi
local mode
for mode in opaque glassy; do
[[ $mode == "$1" ]] || rm -f "$TOGGLES_DIR/window-$mode.lua" || return 1
done
}
if (( $# == 0 )); then
current_mode
exit 0
fi
case $1 in
opaque) LABEL="Opaque windows" ;;
transparent) LABEL="Transparent windows" ;;
glassy) LABEL="Glassy windows" ;;
*)
echo "Usage: omarchy-hyprland-window-transparency [opaque|transparent|glassy]" >&2
exit 1
;;
esac
# A theme switch renders the next theme's terminal files from the saved choice
# and then replaces the current theme, so the choice and the glass written into
# the current theme are changed under its lock, or the switch could drop them.
exec 9>"${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock"
flock 9
# A choice whose glass cannot be written is taken back, so the saved choice
# is always the one on screen.
previous=$(current_mode)
save_mode "$1" || exit 1
if ! omarchy-theme-set-glass; then
save_mode "$previous"
omarchy-theme-set-glass
exit 1
fi
flock -u 9
hyprctl reload >/dev/null || exit 1
omarchy-restart-terminal
omarchy-theme-set-foot
omarchy-restart-btop
omarchy-notification-send -g 󰂵 "$LABEL"
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# omarchy:summary=Print the layout of a workspace: dwindle, scrolling, or floating
# omarchy:args=[workspace-id]
if [[ -n $1 ]]; then
WORKSPACE_ID=$1
TILED_LAYOUT=$(hyprctl workspaces -j | jq -r --arg id "$WORKSPACE_ID" 'first(.[] | select((.id | tostring) == $id) | .tiledLayout) // empty')
else
read -r WORKSPACE_ID TILED_LAYOUT < <(hyprctl activeworkspace -j | jq -r '"\(.id) \(.tiledLayout // "")"')
fi
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
# Hyprland only reports the tiled layout, and floating is not one, so the mode is
# read back from the file omarchy-hyprland-workspace-layout-toggle wrote. Files
# saved before floating existed name no mode, and a workspace without a mode of
# its own follows the default layout.
if [[ $WORKSPACE_ID =~ ^[0-9]+$ ]] && (( WORKSPACE_ID >= 1 )); then
MODE=$(sed -n 's/.*mode = "\([a-z]*\)".*/\1/p' "$MODES_DIR/$WORKSPACE_ID.lua" 2>/dev/null)
if [[ -z $MODE && -f $MODES_DIR/default.lua ]]; then
MODE=$(omarchy-hyprland-workspace-layout-default)
fi
fi
MODE=${MODE:-$TILED_LAYOUT}
echo "${MODE:-dwindle}"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# omarchy:summary=Show or set the layout of workspaces without one of their own: dwindle, scrolling, or floating
# omarchy:args=[dwindle|scrolling|floating]
# omarchy:examples=omarchy hyprland workspace layout default | omarchy hyprland workspace layout default scrolling
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
DEFAULT_FILE="$MODES_DIR/default.lua"
# Without a saved default, Hyprland's own layout is the default, which the
# user's looknfeel may have set.
if (( $# == 0 )); then
MODE=$(sed -n 's/.*default = "\([a-z]*\)".*/\1/p' "$DEFAULT_FILE" 2>/dev/null)
[[ -n $MODE ]] || MODE=$(hyprctl getoption general.layout -j 2>/dev/null | jq -r '.str // empty')
echo "${MODE:-dwindle}"
exit 0
fi
case $1 in
dwindle) LABEL=tiling ;;
scrolling | floating) LABEL=$1 ;;
*)
echo "Usage: omarchy-hyprland-workspace-layout-default [dwindle|scrolling|floating]" >&2
exit 1
;;
esac
# Saved even for dwindle, so the choice also wins over a layout set in the
# user's own looknfeel. Workspaces given a layout with Super + L keep theirs.
mkdir -p "$MODES_DIR"
printf 'o.workspace_mode({ default = "%s" })\n' "$1" >"$DEFAULT_FILE"
hyprctl reload >/dev/null
omarchy-shell -q omarchy.workspaces refresh
omarchy-notification-send -g 󱂬 "New workspaces use the $LABEL layout"
+62 -14
View File
@@ -1,21 +1,69 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Toggle the layout on the current active workspace between dwindle and scrolling # omarchy:summary=Cycle the current active workspace through the dwindle, scrolling, and floating layouts
# omarchy:args=[back]
DIRECTION=${1:-next}
if [[ $DIRECTION != "next" && $DIRECTION != "back" ]]; then
echo "Usage: omarchy-hyprland-workspace-layout-toggle [back]" >&2
exit 1
fi
ACTIVE_WORKSPACE=$(hyprctl activeworkspace -j | jq -r '.id') ACTIVE_WORKSPACE=$(hyprctl activeworkspace -j | jq -r '.id')
[[ $ACTIVE_WORKSPACE =~ ^-?[0-9]+$ ]] || exit 1 # Named workspaces report a negative id, and Hyprland reads a leading "-" as a
CURRENT_LAYOUT=$(hyprctl activeworkspace -j | jq -r '.tiledLayout') # selector relative to the current workspace: targeting one lands on workspace 1
LAYOUTS_DIR="$HOME/.local/state/omarchy/workspace-layouts" # and reconfigures that instead.
LAYOUT_FILE="$LAYOUTS_DIR/$ACTIVE_WORKSPACE.lua" [[ $ACTIVE_WORKSPACE =~ ^[0-9]+$ ]] || exit 1
MODES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/workspace-layouts"
MODE_FILE="$MODES_DIR/$ACTIVE_WORKSPACE.lua"
case "$CURRENT_LAYOUT" in CURRENT_MODE=$(omarchy-hyprland-workspace-layout-current)
dwindle) NEW_LAYOUT=scrolling ;;
*) NEW_LAYOUT=dwindle ;;
esac
mkdir -p "$LAYOUTS_DIR" if [[ $DIRECTION == "back" ]]; then
printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKSPACE" "$NEW_LAYOUT" >"$LAYOUT_FILE" case "$CURRENT_MODE" in
dwindle) NEW_MODE=floating ;;
floating) NEW_MODE=scrolling ;;
*) NEW_MODE=dwindle ;;
esac
else
case "$CURRENT_MODE" in
dwindle) NEW_MODE=scrolling ;;
scrolling) NEW_MODE=floating ;;
*) NEW_MODE=dwindle ;;
esac
fi
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \ APPLY="o.workspace_mode({ workspace = \"$ACTIVE_WORKSPACE\", mode = \"$NEW_MODE\" })"
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_LAYOUT" # Saving a mode that did not apply would leave the file naming one layout while
# the screen shows another, and restore it at the next login.
APPLIED=0
if hyprctl eval "$APPLY" >/dev/null 2>&1; then
APPLIED=1
fi
if (( ! APPLIED )); then
omarchy-notification-send -g 󱂬 "Workspace layout needs a Hyprland reload"
elif [[ $NEW_MODE == "$(omarchy-hyprland-workspace-layout-default)" ]] && rm -f "$MODE_FILE"; then
# Cycled onto the default, a workspace goes back to following it, so a later
# change of the default reaches it too.
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_MODE"
elif mkdir -p "$MODES_DIR" && printf '%s\n' "$APPLY" >"$MODE_FILE"; then
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_MODE"
else
# The mode is on screen but nothing will bring it back, and the next press
# reads the mode from this file, so say so rather than counting it as set.
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_MODE but could not be saved"
fi
# The bar shows the focused workspace's layout.
if (( APPLIED )); then
omarchy-shell -q omarchy.workspaces refresh
fi
# Titlebars and shadows are set up while the config loads, and only
# when something floats, so the first floating workspace needs a reload to get
# them. Once the titlebar plugin is in, later ones have them already.
if (( APPLIED )) && [[ $NEW_MODE == "floating" ]] && ! hyprctl plugin list -j 2>/dev/null | jq -e 'any(.[]?; .name == "hyprbars")' >/dev/null 2>&1; then
hyprctl reload >/dev/null
fi
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# omarchy:summary=Configure, inspect, or switch input methods
# omarchy:args=<status|cycle [back]|configure --seed|--defaults>
# omarchy:hidden=true
set -euo pipefail
if [[ ${1:-} == "cycle" ]]; then
direction=${2:-next}
if [[ $direction != "next" && $direction != "back" ]]; then
echo "Usage: omarchy-input-method cycle [back]" >&2
exit 1
fi
# The shell's reader retains a choice made while nothing has focus.
if [[ $(omarchy-shell shell cycleInput "$direction") != "ok" ]]; then
echo "The shell's input reader is not running" >&2
exit 1
fi
else
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/configure.py" "$@"
fi
+3
View File
@@ -92,6 +92,9 @@ phoenix)
;; ;;
rust) rust)
echo -e "Installing Rust...\n" echo -e "Installing Rust...\n"
# Crates that generate C bindings with bindgen, including Ruby gems built
# with rb-sys, load libclang from clang.
omarchy-pkg-add clang
bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs)" -- -y bash -c "$(curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs)" -- -y
;; ;;
java) java)
+2 -4
View File
@@ -15,17 +15,15 @@ if gum confirm "Are you sure you want to restore all preinstalled web apps, TUI
aether \ aether \
cliamp \ cliamp \
libreoffice-fresh \ libreoffice-fresh \
xournalpp \
pinta \ pinta \
obsidian \ obsidian \
obs-studio \
kdenlive \
moonlight-qt \ moonlight-qt \
omacut \ omacut \
monologue \ monologue \
omacalc \ omacalc \
omawrite \ omawrite \
hype; then hype \
signed; then
echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again." echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again."
exit 1 exit 1
fi fi
+1 -1
View File
@@ -555,7 +555,7 @@ output_binding_records_uncached() {
keybindings_cache_key() { keybindings_cache_key() {
{ {
printf 'v14\n' printf 'v15\n'
hyprctl devices 2>/dev/null | grep -F 'active keymap:' hyprctl devices 2>/dev/null | grep -F 'active keymap:'
cat "$key_names_file" 2>/dev/null cat "$key_names_file" 2>/dev/null
hyprctl binds 2>/dev/null hyprctl binds 2>/dev/null
+46 -4
View File
@@ -1,9 +1,9 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Pick one option from a menu # omarchy:summary=Pick one or multiple options from a menu
# omarchy:group=menu # omarchy:group=menu
# omarchy:name=select # omarchy:name=select
# omarchy:args=prompt [option...] [-- menu args...] # omarchy:args=prompt [option...] [-- --multiple --selected <value> --on-change <command-json> --change-key <key> --width <pixels> --maxheight <pixels>]
# omarchy:examples=omarchy menu select Format jpg png|omarchy-menu-select Resolution 4k 1080p 720p -- --width 400 # omarchy:examples=omarchy menu select Format jpg png|omarchy-menu-select Resolution 4k 1080p 720p -- --width 400
# An option may lead with an icon, as "<glyph><TAB><label>", and may trail a # An option may lead with an icon, as "<glyph><TAB><label>", and may trail a
@@ -12,6 +12,12 @@
# alone; an option with a subtext returns "<label><TAB><subtext>", so callers # alone; an option with a subtext returns "<label><TAB><subtext>", so callers
# with same-named rows get the subtext back as the stable key. # with same-named rows get the subtext back as the stable key.
# With --multiple, clicking/Enter toggles rows and Apply returns a JSON array.
# --on-change takes a JSON command array; each toggle runs it with the selection
# JSON appended, applies immediately, and needs no Apply row. A successful
# command may print a JSON array of actual selections to update the checkmarks.
# --selected takes a returned value (label plus any subtext), and can be repeated.
set -euo pipefail set -euo pipefail
if (( $# < 1 )); then if (( $# < 1 )); then
@@ -25,12 +31,43 @@ shift
options=() options=()
menu_width="" menu_width=""
menu_maxheight="" menu_maxheight=""
multiple=0
on_change="[]"
change_key=""
selected=()
while (( $# > 0 )); do while (( $# > 0 )); do
if [[ $1 == "--" ]]; then if [[ $1 == "--" ]]; then
shift shift
while (( $# > 0 )); do while (( $# > 0 )); do
case "$1" in case "$1" in
--on-change)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --on-change requires a JSON command array" >&2
exit 1
fi
on_change="$1"
;;
--change-key)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --change-key requires a value" >&2
exit 1
fi
change_key="$1"
;;
--multiple)
multiple=1
;;
--selected)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --selected requires a value" >&2
exit 1
fi
selected+=("$1")
;;
--width) --width)
shift shift
if (( $# == 0 )); then if (( $# == 0 )); then
@@ -73,20 +110,25 @@ rm -f "$done_file"
trap 'rm -f "$selection_file" "$done_file"' EXIT trap 'rm -f "$selection_file" "$done_file"' EXIT
options_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${options[@]}") options_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${options[@]}")
selected_json=$(perl -MEncode=decode -MJSON::PP=encode_json -e 'print encode_json([map { decode("UTF-8", $_) } @ARGV])' "${selected[@]}")
payload=$(perl -MEncode=decode -MJSON::PP=encode_json,decode_json -e ' payload=$(perl -MEncode=decode -MJSON::PP=encode_json,decode_json -e '
my $payload = { my $payload = {
mode => "select", mode => "select",
prompt => decode("UTF-8", $ARGV[0]), prompt => decode("UTF-8", $ARGV[0]),
options => decode_json($ARGV[1]), options => decode_json($ARGV[1]),
multiple => $ARGV[6] ? JSON::PP::true : JSON::PP::false,
selected => decode_json($ARGV[7]),
onChange => decode_json($ARGV[8]),
changeKey => $ARGV[9],
selectionFile => decode("UTF-8", $ARGV[2]), selectionFile => decode("UTF-8", $ARGV[2]),
doneFile => decode("UTF-8", $ARGV[3]) doneFile => decode("UTF-8", $ARGV[3])
}; };
$payload->{width} = int($ARGV[4]) if length($ARGV[4] // ""); $payload->{width} = int($ARGV[4]) if length($ARGV[4] // "");
$payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // ""); $payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // "");
print encode_json($payload) print encode_json($payload)
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight") ' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight" "$multiple" "$selected_json" "$on_change" "$change_key")
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null omarchy-shell shell summon omarchy.menu "$payload" >/dev/null || exit 2
while [[ ! -e $done_file ]]; do while [[ ! -e $done_file ]]; do
sleep 0.05 sleep 0.05
+1
View File
@@ -56,6 +56,7 @@ if [[ -z ${profile:-} ]] || ! profile_available "$profile"; then
fi fi
powerprofilesctl set "$profile" || exit powerprofilesctl set "$profile" || exit
omarchy-shell -q battery refreshPowerProfile >/dev/null 2>&1 || true
if [[ -n $requested_profile ]]; then if [[ -n $requested_profile ]]; then
mkdir -p "$state_dir" mkdir -p "$state_dir"
+4 -2
View File
@@ -187,11 +187,11 @@ tips=(
"Super + Space opens the Omarchy menu for apps, settings, and more" "Super + Space opens the Omarchy menu for apps, settings, and more"
"Super + K shows all the key bindings" "Super + K shows all the key bindings"
"Super is the Windows or command key on your keyboard" "Super is the Windows or command key on your keyboard"
"Use Xournal++ to sign PDFs" "Sign PDFs and fill them in with Signed"
"Share files with phones and laptops using LocalSend" "Share files with phones and laptops using LocalSend"
"Turn any website into an app with Install > Web App in the menu" "Turn any website into an app with Install > Web App in the menu"
"Super + Return opens a terminal, Super + Shift + Return the browser" "Super + Return opens a terminal, Super + Shift + Return the browser"
"Edit images with Pinta, videos with Kdenlive, docs with LibreOffice" "Edit images with Pinta, trim videos with Omacut, docs with LibreOffice"
"Super + Ctrl + Print grabs text off the screen with OCR" "Super + Ctrl + Print grabs text off the screen with OCR"
"Print takes a screenshot, Alt + Print records the screen" "Print takes a screenshot, Alt + Print records the screen"
"Set a reminder with Super + Ctrl + R" "Set a reminder with Super + Ctrl + R"
@@ -615,6 +615,8 @@ keyboard_form() {
# re-key below. Confirm it so a stray Ctrl+C doesn't bounce the machine. # re-key below. Confirm it so a stray Ctrl+C doesn't bounce the machine.
confirm_reboot && exec systemctl reboot confirm_reboot && exec systemctl reboot
done done
omarchy_persist_input_selection
} }
confirm_reboot() { confirm_reboot() {
+2 -4
View File
@@ -34,15 +34,13 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
aether \ aether \
cliamp \ cliamp \
libreoffice-fresh \ libreoffice-fresh \
xournalpp \
pinta \ pinta \
obsidian \ obsidian \
obs-studio \
kdenlive \
moonlight-qt \ moonlight-qt \
omacut \ omacut \
monologue \ monologue \
omacalc \ omacalc \
omawrite \ omawrite \
hype hype \
signed
fi fi
+22 -1
View File
@@ -2,9 +2,30 @@
# omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings. # omarchy:summary=Restart the XCompose input method service (fcitx5) to apply new compose key settings.
set -euo pipefail
# A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's # A fcitx5 running outside the unit owns org.fcitx.Fcitx5, so the unit's
# instance would exit on arrival and the stale process would keep serving the # instance would exit on arrival and the stale process would keep serving the
# old ~/.XCompose -- a restart that reports success and changes nothing. # old ~/.XCompose -- a restart that reports success and changes nothing.
systemctl --user stop omarchy-fcitx5.service systemctl --user stop omarchy-fcitx5.service
pkill -x fcitx5 >/dev/null 2>&1 || true pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true
for attempt in {1..50}; do
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then
break
fi
sleep 0.1
done
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
# An activation concurrent with stop may have restarted the unit.
systemctl --user stop omarchy-fcitx5.service
pkill -u "$UID" -x fcitx5 >/dev/null 2>&1 || true
for attempt in {1..50}; do
if ! pgrep -u "$UID" -x fcitx5 >/dev/null; then break; fi
sleep 0.1
done
fi
if pgrep -u "$UID" -x fcitx5 >/dev/null; then
echo "Fcitx did not stop; input service was not restarted." >&2
exit 1
fi
systemctl --user start omarchy-fcitx5.service systemctl --user start omarchy-fcitx5.service
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# omarchy:summary=Select input methods for Japanese, Korean, or Chinese typing
# omarchy:args=[mozc|hangul|pinyin|chewing]
# omarchy:examples=omarchy setup input | omarchy setup input mozc
# omarchy:requires-sudo=true
set -euo pipefail
if (( $# > 1 )); then
echo "Usage: omarchy setup input [mozc|hangul|pinyin|chewing]" >&2
exit 1
fi
method=${1:-}
if [[ -z $method ]]; then
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" input
exit
fi
case "$method" in
mozc | hangul | pinyin | chewing) ;;
*) echo "Unknown input method: $method" >&2; exit 1 ;;
esac
mapfile -t packages < <(jq -r --arg method "$method" '.[$method].packages[]' "$OMARCHY_PATH/default/input-methods/presets.json")
omarchy-pkg-add "${packages[@]}"
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" add "$method"
echo "Input method added. Use Super+I or the Japanese/Korean input key to switch; existing custom Fcitx keys are preserved."
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# omarchy:summary=Select the keyboard layouts available for switching
set -euo pipefail
/usr/bin/python3 "$OMARCHY_PATH/default/input-methods/typing_setup.py" keyboard
+30 -3
View File
@@ -462,7 +462,34 @@ root_dispatch() {
esac esac
} }
# The bar indicator only probes on demand, so every grant change pushes a
# refresh. Root's expiry timer can't reach the user's shell, so a user timer
# refreshes once the deadline has passed. Both are best effort: a stale red
# indicator still revokes cleanly when clicked.
refresh_indicator() {
/usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
}
# A failed enable can still change the grant (a renewal that rolls back removes
# the previous policy), so refresh whatever the privileged action returned.
change_grant() {
local status=0
/usr/bin/sudo "$@" || status=$?
refresh_indicator
return "$status"
}
# A wall-clock deadline, like the sudoers rule and root's timer, so expiry
# during suspend still refreshes on resume.
schedule_expiry_refresh() {
[[ $minutes == "permanent" ]] && return 0
/usr/bin/systemd-run --user --quiet --collect --on-calendar="@$((EPOCHSECONDS + 10#$minutes * 60 + 2))" \
--timer-property=AccuracySec=1s \
-- /usr/bin/omarchy-shell -q omarchy.indicators refresh >/dev/null 2>&1 || true
}
show_enabled() { show_enabled() {
schedule_expiry_refresh
if [[ $minutes == "permanent" ]]; then if [[ $minutes == "permanent" ]]; then
echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it." echo "Passwordless sudo has been ENABLED permanently. Run this command again to disable it."
else else
@@ -542,12 +569,12 @@ else
fi fi
fi fi
if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then if (( status == 0 )) && { (($# == 0)) || [[ $minutes == "--disable" ]]; }; then
/usr/bin/sudo -n -N -- "$INSTALLED_SELF" __disable "$uid" change_grant -n -N -- "$INSTALLED_SELF" __disable "$uid"
echo "Passwordless sudo has been DISABLED. Sudo will require a password again." echo "Passwordless sudo has been DISABLED. Sudo will require a password again."
elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then elif (( status == STATUS_INACTIVE )) && [[ $minutes == "--disable" ]]; then
echo "Passwordless sudo is already DISABLED." echo "Passwordless sudo is already DISABLED."
elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then elif (( status == 0 )) && [[ $minutes != "permanent" ]]; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes" change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
show_enabled show_enabled
else else
if (( status != 0 && status != STATUS_INACTIVE )); then if (( status != 0 && status != STATUS_INACTIVE )); then
@@ -587,7 +614,7 @@ else
echo "" echo ""
if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then if /usr/bin/gum confirm "Enable passwordless sudo $duration? This is a significant security risk!"; then
/usr/bin/sudo -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes" change_grant -N -- "$INSTALLED_SELF" __enable "$uid" "$minutes"
echo "" echo ""
show_enabled show_enabled
else else
+2 -2
View File
@@ -20,6 +20,6 @@ fi
# Create symlink to the new background media # Create symlink to the new background media
ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK" ln -nsf "$BACKGROUND" "$CURRENT_BACKGROUND_LINK"
# Update the live shell background immediately when it is running. The # Update the live shell background when it is running. Nothing watches the
# background plugin also polls this symlink, but IPC avoids the visible delay. # symlink, so this is how the shell learns of the new background.
omarchy-shell -q background set "$BACKGROUND" omarchy-shell -q background set "$BACKGROUND"
+8 -5
View File
@@ -207,11 +207,14 @@ set_theme_background() {
shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true shell_ipc shell applyTheme "$colors_payload" "$shell_payload" || true
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK" ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
( (
if ! omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared"; then # Played or not, the shell gets the new still: it still holds the old
if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" && # theme's path, which the theme swap above deleted, and loads it again
$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then # when OWE hands the desktop back after the intro. While the intro plays
shell_ipc background setInstant "$new_background" || true # the shell's background is suspended under it, so the swap is unseen.
fi omarchy-theme-bg-boot-intro --theme-switch "$new_background" "$theme_id" "$intro_prepared" || true
if [[ $(readlink -f "$CURRENT_BACKGROUND_LINK") == "$(readlink -f "$new_background")" &&
$(stat -Lc '%d:%i' "$CURRENT_THEME_PATH" 2>/dev/null) == "$theme_id" ]]; then
shell_ipc background setInstant "$new_background" || true
fi fi
shell_ipc shell finishThemeIntro "$theme_id" || true shell_ipc shell finishThemeIntro "$theme_id" || true
wait_for_theme_intro themeIntroStatus "$theme_id" "pending" wait_for_theme_intro themeIntroStatus "$theme_id" "pending"
+12 -1
View File
@@ -1,9 +1,10 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Apply current Omarchy theme colors to running Foot terminals # omarchy:summary=Apply current Omarchy theme colors and background alpha to running Foot terminals
# omarchy:hidden=true # omarchy:hidden=true
colors_toml=$HOME/.local/state/omarchy/current/theme/colors.toml colors_toml=$HOME/.local/state/omarchy/current/theme/colors.toml
foot_ini=$HOME/.local/state/omarchy/current/theme/foot.ini
if [[ ! -f $colors_toml ]] || ! pgrep -x foot >/dev/null; then if [[ ! -f $colors_toml ]] || ! pgrep -x foot >/dev/null; then
exit 0 exit 0
@@ -12,6 +13,16 @@ fi
foot_osc=$(omarchy-theme-osc "$colors_toml") foot_osc=$(omarchy-theme-osc "$colors_toml")
[[ -n $foot_osc ]] || exit 0 [[ -n $foot_osc ]] || exit 0
# foot takes a background alpha as an urxvt-style [percent] prefix, so a theme
# going see-through or back, as with Glassy windows, reaches running terminals.
# The last alpha in the theme's foot.ini wins, as it does when foot reads it.
alpha=$(sed -n 's/^[[:space:]]*alpha[[:space:]]*=[[:space:]]*\([0-9.]*\).*/\1/p' "$foot_ini" 2>/dev/null | tail -n 1)
background=$(omarchy-theme-color --file "$colors_toml" background 2>/dev/null)
if [[ -n $background ]]; then
percent=$(awk -v alpha="${alpha:-1}" 'BEGIN { printf "%d", alpha * 100 + 0.5 }')
foot_osc+=$(printf '\033]11;[%d]%s\007' "$percent" "$background")
fi
for foot_pid in $(pgrep -x foot); do for foot_pid in $(pgrep -x foot); do
for child_pid in $(pgrep -P "$foot_pid"); do for child_pid in $(pgrep -P "$foot_pid"); do
tty=$(readlink "/proc/$child_pid/fd/1" 2>/dev/null) tty=$(readlink "/proc/$child_pid/fd/1" 2>/dev/null)
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
# omarchy:summary=Give terminals, btop, and GTK apps a see-through background while windows are glassy
# omarchy:args=[theme-dir]
# omarchy:hidden=true
# A Hyprland opacity rule fades a terminal's text along with its background,
# and foot turns compositor blur down unless its own config asks for it, so the
# glass belongs in each terminal's theme file, as in the glassy Starship
# (#14726). The lines sit between markers so leaving glassy takes them out
# again, and a theme that sets its own opacity keeps it.
THEME_DIR="${1:-$HOME/.local/state/omarchy/current/theme}"
TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr"
GLASSY=false
[[ -f $TOGGLES_DIR/window-glassy.lua ]] && GLASSY=true
status=0
# A literal for a sed address, whose delimiter is /.
regex() {
sed 's/[][\\.*^$/]/\\&/g' <<<"$1"
}
# A block added to a file that did not end in a newline has to supply one, so
# its begin marker says so, and taking the block out takes that newline back.
# Leaving glassy then gives the file back exactly as it was. Markers come as
# an opening and a closing part, so CSS can wrap them in a comment.
remove_block() {
local file="$1" open="$2" close="$3"
local begin end
begin=$(regex "$open omarchy-glass begin")
end=$(regex "$open omarchy-glass end$close")
[[ -f $file ]] && grep -qF "$open omarchy-glass begin" "$file" || return 0
local added_newline=false
grep -qxF "$open omarchy-glass begin after a line without a newline$close" "$file" && added_newline=true
sed -i "/^$begin/,/^$end\$/d" "$file" || return 1
if [[ $added_newline == "true" && -s $file && -z $(tail -c 1 "$file") ]]; then
truncate -s -1 "$file" || return 1
fi
}
append_block() {
local file="$1" open="$2" close="$3" lines="$4"
if [[ -s $file && -n $(tail -c 1 "$file") ]]; then
printf '\n%s\n%s\n%s\n' "$open omarchy-glass begin after a line without a newline$close" "$lines" "$open omarchy-glass end$close" >>"$file"
else
printf '%s\n%s\n%s\n' "$open omarchy-glass begin$close" "$lines" "$open omarchy-glass end$close" >>"$file"
fi
}
# Each setting comes with a pattern for the theme setting it already, which
# keeps the theme's own; an empty pattern always adds it. Lines that belong in
# a section go in under its header. A file that already has the section gets
# them inside it, since TOML refuses a table declared twice; otherwise the
# header comes along with them.
glass() {
local file="$THEME_DIR/$1" section="$2"
shift 2
[[ -f $file ]] || return 0
remove_block "$file" "#" "" || return 1
[[ $GLASSY == "true" ]] || return 0
local lines=""
while (( $# >= 2 )); do
if [[ -z $1 ]] || ! grep -qE "$1" "$file"; then
lines+="${lines:+$'\n'}$2"
fi
shift 2
done
[[ -n $lines ]] || return 0
# A header may carry trailing space or a comment.
local header="^$(regex "$section")[[:space:]]*(#.*)?\$"
if [[ -n $section ]] && grep -qE "$header" "$file"; then
local block
block=$(mktemp) || return 1
printf '# omarchy-glass begin\n%s\n# omarchy-glass end\n' "$lines" >"$block"
sed -i -E "0,/$header/{/$header/r $block
}" "$file"
local result=$?
rm -f "$block"
return $result
fi
append_block "$file" "#" "" "${section:+$section$'\n'}$lines"
}
# Alacritty's own blur works only on macOS and KDE; Hyprland blurs it anyway.
glass alacritty.toml '[window]' \
'^[[:space:]]*opacity[[:space:]]*=' 'opacity = 0.65' || status=1
glass foot.ini '[colors-dark]' \
'^[[:space:]]*alpha[[:space:]]*=' 'alpha=0.65' \
'^[[:space:]]*blur[[:space:]]*=' 'blur=yes' || status=1
glass kitty.conf '' \
'^[[:space:]]*background_opacity[[:space:]]' 'background_opacity 0.65' || status=1
# btop paints its own background over the terminal's glass. An empty main_bg
# uses the terminal background; the last theme value wins, so removing the
# block restores the theme's colour without changing btop's user config.
glass btop.theme '' \
'^[[:space:]]*theme\[main_bg\][[:space:]]*=[[:space:]]*""' 'theme[main_bg]=""' || status=1
# GTK only asks Hyprland for blur under a CSS backdrop-filter, which Ghostty
# takes as custom CSS and reloads live; its background-blur speaks only KDE's
# blur protocol.
glass ghostty.conf '' \
'^[[:space:]]*background-opacity[[:space:]]*=' 'background-opacity = 0.65' \
'' "gtk-custom-css = $OMARCHY_PATH/default/gtk/glass.css" || status=1
# Other GTK apps, such as Nautilus, read the user's gtk.css at launch, so the
# same backdrop blur is imported there. It follows the mode alone, so a theme
# switch, which names the theme it is rendering, leaves it be.
if (( $# == 0 )); then
gtk_css="${XDG_CONFIG_HOME:-$HOME/.config}/gtk-4.0/gtk.css"
remove_block "$gtk_css" "/*" " */" || status=1
if [[ $GLASSY == "true" ]]; then
{ mkdir -p "${gtk_css%/*}" &&
append_block "$gtk_css" "/*" " */" "@import url(\"file://$OMARCHY_PATH/default/gtk/glass.css\");"; } || status=1
fi
fi
exit $status
+1
View File
@@ -451,3 +451,4 @@ if [[ -f $COLORS_FILE ]]; then
fi fi
apply_shell_section_overrides apply_shell_section_overrides
omarchy-theme-set-glass "$NEXT_THEME_DIR"
+3 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Toggle animations, transparency and other effects that are slow without a GPU # omarchy:summary=Toggle animations, blur and shadows, which are slow without a GPU
# omarchy:args=[toggle|on|off] # omarchy:args=[toggle|on|off]
# omarchy:examples=omarchy toggle animations | omarchy toggle animations off | omarchy toggle animations on # omarchy:examples=omarchy toggle animations | omarchy toggle animations off | omarchy toggle animations on
@@ -16,7 +16,7 @@ off) omarchy-hyprland-toggle no-animations on ;;
esac esac
if omarchy-hyprland-toggle-enabled no-animations; then if omarchy-hyprland-toggle-enabled no-animations; then
omarchy-notification-send -g 󰕟 "Animations disabled" omarchy-notification-send -g 󰗘 "Animations disabled"
else else
omarchy-notification-send -g 󰕟 "Animations enabled" omarchy-notification-send -g 󰗘 "Animations enabled"
fi fi
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# omarchy:summary=Toggle bar visibility without killing the Omarchy shell # omarchy:summary=Toggle bar visibility without killing the Omarchy shell (on hides the bar, off shows it)
# omarchy:args=[toggle|on|off] # omarchy:args=[toggle|on|off]
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on # omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
+2
View File
@@ -1807,6 +1807,8 @@ copy_missing_config_defaults() {
while IFS= read -r -d '' source_path; do while IFS= read -r -d '' source_path; do
rel="${source_path#$source_dir/}" rel="${source_path#$source_dir/}"
is_retired_config_file "$rel" && continue is_retired_config_file "$rel" && continue
# The input migration preserves working shortcuts in existing IME profiles.
[[ $rel == "fcitx5/config" ]] && continue
target_path="$target_dir/$rel" target_path="$target_dir/$rel"
[[ -e $target_path || -L $target_path ]] && continue [[ -e $target_path || -L $target_path ]] && continue
+5
View File
@@ -234,6 +234,11 @@ if [[ -n $MIME_TYPES ]]; then
printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE" printf 'MimeType=%s\n' "$(desktop_string_escape "$MIME_TYPES")" >>"$DESKTOP_FILE"
fi fi
# Floating titlebars name the window after this entry rather than its page title.
if [[ -z $CUSTOM_EXEC ]]; then
printf 'StartupWMClass=%s\n' "$(desktop_string_escape "$(omarchy-webapp-window-class "$APP_URL")")" >>"$DESKTOP_FILE"
fi
chmod +x "$DESKTOP_FILE" chmod +x "$DESKTOP_FILE"
if [[ $INTERACTIVE_MODE == "true" ]]; then if [[ $INTERACTIVE_MODE == "true" ]]; then
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# omarchy:summary=Print the window class Chromium gives a web app opened at a URL
# omarchy:args=<url>
# omarchy:hidden=true
# Chromium names an --app window after the URL's host and path, so a web app's
# launcher can name its window in StartupWMClass, which titlebars use to show
# the app's name rather than the page title.
url=${1:-}
if [[ -z $url ]]; then
echo "Usage: omarchy-webapp-window-class <url>" >&2
exit 1
fi
address=${url#*://}
address=${address%%[?#]*}
host=${address%%/*}
path=${address:${#host}}
host=${host##*@}
host=${host%%:*}
printf 'chrome-%s_%s-Default\n' "${host,,}" "${path:-/}" | sed 's|/|_|g'
+3
View File
@@ -0,0 +1,3 @@
# Keep Pinyin offline without prompting to enable cloud prediction.
FirstRun=False
CloudPinyinEnabled=False
+2
View File
@@ -0,0 +1,2 @@
[TriggerKey]
0=Super+semicolon
+1
View File
@@ -0,0 +1 @@
Allow Overriding System XKB Settings=False
+21
View File
@@ -0,0 +1,21 @@
# Omarchy binds Super+I. Leave Ctrl+Space to tmux/Herdr and other Super keys to the desktop.
[Hotkey/TriggerKeys]
0=Zenkaku_Hankaku
1=Hangul
[Hotkey/ActivateKeys]
0=Henkan
[Hotkey/DeactivateKeys]
0=Muhenkan
# A lone Shift leaves an engine like Pinyin for Latin input and returns to it.
[Hotkey/AltTriggerKeys]
0=Shift_L
[Hotkey/EnumerateGroupForwardKeys]
[Hotkey/EnumerateGroupBackwardKeys]
[Behavior]
ActiveByDefault=False
+6 -3
View File
@@ -6,9 +6,12 @@
name = "terminal" name = "terminal"
[theme.custom] [theme.custom]
# The active tab is drawn as panel_bg text on an accent background, so panel_bg # The tab bar sits on the terminal's own background, like tmux's bg=default, so
# has to be dark for it to read - same colors as status-left's "#[fg=black,bg=blue]" # it turns see-through with the rest of the terminal under Glassy windows. With
panel_bg = "black" # no panel_bg the active tab's text falls back to surface_dim, so that is black
# for dark text on the accent - same colors as status-left's "#[fg=black,bg=blue]"
panel_bg = "reset"
surface_dim = "black"
[terminal] [terminal]
# Matches -c "#{pane_current_path}" on every split, window, and session # Matches -c "#{pane_current_path}" on every split, window, and session
+1 -1
View File
@@ -7,7 +7,7 @@
-- input = { -- input = {
-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt. -- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
-- kb_layout = "us,dk,eu", -- kb_layout = "us,dk,eu",
-- kb_options = "compose:caps,shift:both_capslock_cancel,grp:alts_toggle", -- kb_options = "omarchy:shift_caps_compose,grp:alts_toggle",
-- --
-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards). -- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
-- kb_variant = "intl", -- kb_variant = "intl",
+4 -4
View File
@@ -6,7 +6,7 @@
}, },
"bar": { "bar": {
"position": "top", "position": "top",
"transparent": false, "background": "opaque",
"centerAnchor": "omarchy.clock", "centerAnchor": "omarchy.clock",
"layout": { "layout": {
"left": [ "left": [
@@ -30,9 +30,6 @@
{ {
"id": "omarchy.elsewhen" "id": "omarchy.elsewhen"
}, },
{
"id": "omarchy.keyboard-layout"
},
{ {
"id": "omarchy.weather" "id": "omarchy.weather"
}, },
@@ -41,6 +38,9 @@
} }
], ],
"right": [ "right": [
{
"id": "omarchy.keyboard-layout"
},
{ {
"id": "omarchy.tray" "id": "omarchy.tray"
}, },
-268
View File
@@ -1,268 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--The Xournal++ settings file. Do not edit this file! Most settings are available in the Settings dialog, the others are commented in this file, but handle with care!-->
<settings>
<property name="pressureSensitivity" value="true"/>
<property name="minimumPressure" value="0.05"/>
<property name="pressureMultiplier" value="1"/>
<property name="zoomGesturesEnabled" value="true"/>
<property name="selectedToolbar" value="Portrait"/>
<property name="lastSavePath" value=""/>
<property name="lastOpenPath" value=""/>
<property name="lastImagePath" value=""/>
<property name="edgePanSpeed" value="20"/>
<property name="edgePanMaxMult" value="5"/>
<property name="zoomStep" value="10"/>
<property name="zoomStepScroll" value="2"/>
<property name="displayDpi" value="72"/>
<property name="mainWndWidth" value="800"/>
<property name="mainWndHeight" value="600"/>
<property name="maximized" value="true"/>
<property name="showToolbar" value="true"/>
<property name="showSidebar" value="true"/>
<property name="sidebarWidth" value="150"/>
<property name="sidebarNumberingStyle" value="1"/>
<property name="sidebarOnRight" value="false"/>
<property name="scrollbarOnLeft" value="false"/>
<property name="menubarVisible" value="true"/>
<property name="filepathShownInTitlebar" value="false"/>
<property name="numColumns" value="1"/>
<property name="numRows" value="1"/>
<property name="viewFixedRows" value="false"/>
<property name="showPairedPages" value="false"/>
<property name="layoutVertical" value="false"/>
<property name="layoutRightToLeft" value="false"/>
<property name="layoutBottomToTop" value="false"/>
<property name="numPairsOffset" value="1"/>
<!--The icon theme, allowed values are "disabled", "onDrawOfLastPage", and "onScrollOfLastPage"-->
<property name="emptyLastPageAppend" value="disabled"/>
<property name="presentationMode" value="false"/>
<!--Which GUI elements are shown in default view mode, separated by a colon (,)-->
<property name="defaultViewModeAttributes" value="showMenubar,showToolbar,showSidebar"/>
<!--Which GUI elements are shown in fullscreen view mode, separated by a colon (,)-->
<property name="fullscreenViewModeAttributes" value="goFullscren,showToolbar,showSidebar"/>
<!--Which GUI elements are shown in presentation view mode, separated by a colon (,)-->
<property name="presentationViewModeAttributes" value="goFullscren,showToolbar"/>
<!--The cursor icon used with a stylus, allowed values are "none", "dot", "big", "arrow"-->
<property name="stylusCursorType" value="dot"/>
<!--The eraser cursor visibility used with a stylus, allowed values are "never", "always", "hover", "touch"-->
<property name="eraserVisibility" value="always"/>
<!--The icon theme, allowed values are "iconsColor", "iconsLucide"-->
<property name="iconTheme" value="iconsColor"/>
<!--Dark/light mode, allowed values are "useSystem", "forceLight", "forceDark"-->
<property name="themeVariant" value="forceDark"/>
<property name="highlightPosition" value="false"/>
<property name="cursorHighlightColor" value="2164260608"/>
<property name="cursorHighlightBorderColor" value="2147483903"/>
<property name="cursorHighlightRadius" value="30"/>
<property name="cursorHighlightBorderWidth" value="0"/>
<property name="useStockIcons" value="false"/>
<property name="disableScrollbarFadeout" value="false"/>
<property name="disableAudio" value="false"/>
<!--Hides scroolbars in the main window, allowed values: "none", "horizontal", "vertical", "both"-->
<property name="scrollbarHideType" value="none"/>
<property name="autoloadMostRecent" value="false"/>
<property name="autoloadPdfXoj" value="true"/>
<property name="defaultSaveName" value="%F-Note-%H-%M"/>
<property name="defaultPdfExportName" value="%{name}_annotated"/>
<property name="autosaveEnabled" value="true"/>
<property name="autosaveTimeout" value="3"/>
<property name="addHorizontalSpace" value="false"/>
<property name="addHorizontalSpaceAmount" value="150"/>
<property name="addVerticalSpace" value="false"/>
<property name="addVerticalSpaceAmount" value="150"/>
<property name="drawDirModsEnabled" value="false"/>
<property name="drawDirModsRadius" value="50"/>
<property name="snapRotation" value="true"/>
<property name="snapRotationTolerance" value="0.3"/>
<property name="snapGrid" value="true"/>
<property name="snapGridTolerance" value="0.5"/>
<property name="snapGridSize" value="14.17"/>
<property name="strokeRecognizerMinSize" value="40"/>
<property name="touchDrawing" value="false"/>
<property name="gtkTouchInertialScrolling" value="true"/>
<property name="pressureGuessing" value="false"/>
<property name="selectionBorderColor" value="4294901760"/>
<property name="backgroundColor" value="4292664021"/>
<property name="selectionMarkerColor" value="4285702095"/>
<property name="touchZoomStartThreshold" value="0"/>
<property name="pageRerenderThreshold" value="5"/>
<!--The count of rendered PDF pages which will be cached.-->
<property name="pdfPageCacheSize" value="10"/>
<property name="preloadPagesBefore" value="3"/>
<property name="preloadPagesAfter" value="5"/>
<property name="eagerPageCleanup" value="true"/>
<!--Config for new pages-->
<property name="pageTemplate" value="xoj/template&#10;copyLastPageSettings=true&#10;size=595.275591x841.889764&#10;backgroundType=lined&#10;backgroundColor=#ffffff&#10;"/>
<property name="sizeUnit" value=""/>
<property name="audioFolder" value=""/>
<property name="audioInputDevice" value="-1"/>
<property name="audioOutputDevice" value="-1"/>
<property name="audioSampleRate" value="44100"/>
<property name="audioGain" value="1"/>
<property name="defaultSeekTime" value="5"/>
<property name="pluginEnabled" value=""/>
<property name="pluginDisabled" value=""/>
<property name="strokeFilterIgnoreTime" value="150"/>
<property name="strokeFilterIgnoreLength" value="1"/>
<property name="strokeFilterSuccessiveTime" value="500"/>
<property name="strokeFilterEnabled" value="false"/>
<property name="doActionOnStrokeFiltered" value="false"/>
<property name="trySelectOnStrokeFiltered" value="false"/>
<property name="snapRecognizedShapesEnabled" value="false"/>
<property name="restoreLineWidthEnabled" value="false"/>
<property name="numIgnoredStylusEvents" value="0"/>
<property name="inputSystemTPCButton" value="false"/>
<property name="inputSystemDrawOutsideWindow" value="true"/>
<property name="preferredLocale" value=""/>
<property name="stabilizerAveragingMethod" value="0"/>
<property name="stabilizerPreprocessor" value="0"/>
<property name="stabilizerBuffersize" value="20"/>
<property name="stabilizerSigma" value="0.5"/>
<property name="stabilizerDeadzoneRadius" value="1.3"/>
<property name="stabilizerDrag" value="0.4"/>
<property name="stabilizerMass" value="5"/>
<property name="stabilizerCuspDetection" value="true"/>
<property name="stabilizerFinalizeStroke" value="true"/>
<property name="latexSettings.autoCheckDependencies" value="true"/>
<property name="latexSettings.defaultText" value="x^2"/>
<property name="latexSettings.globalTemplatePath" value="/usr/share/xournalpp/resources/default_template.tex"/>
<property name="latexSettings.genCmd" value="pdflatex -halt-on-error -interaction=nonstopmode '{}'"/>
<property name="latexSettings.sourceViewThemeId" value=""/>
<property name="latexSettings.editorFont" value="Monospace 12"/>
<property name="latexSettings.useCustomEditorFont" value="false"/>
<property name="latexSettings.editorWordWrap" value="true"/>
<property name="latexSettings.sourceViewAutoIndent" value="true"/>
<property name="latexSettings.sourceViewSyntaxHighlight" value="true"/>
<property name="latexSettings.sourceViewShowLineNumbers" value="false"/>
<property name="font" font="Sans" size="12"/>
<data name="buttonConfig">
<data name="default">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
<attribute name="size" type="string" value="thin"/>
<attribute name="tool" type="string" value="pen"/>
</data>
<data name="eraser">
<attribute name="eraserMode" type="string" value="none"/>
<attribute name="size" type="string" value="none"/>
<attribute name="tool" type="string" value="eraser"/>
</data>
<data name="middle">
<attribute name="tool" type="string" value="hand"/>
</data>
<data name="right">
<attribute name="tool" type="string" value="none"/>
</data>
<data name="stylus">
<attribute name="tool" type="string" value="none"/>
</data>
<data name="stylus2">
<attribute name="tool" type="string" value="none"/>
</data>
<data name="touch">
<attribute name="device" type="string" value=""/>
<attribute name="disableDrawing" type="boolean" value="false"/>
<attribute name="tool" type="string" value="none"/>
</data>
</data>
<data name="deviceClasses">
<data name="Wayland Finger Scrolling">
<attribute name="deviceClass" type="int" value="1"/>
<attribute name="deviceSource" type="int" value="6"/>
</data>
<data name="Wayland Pointer">
<attribute name="deviceClass" type="int" value="1"/>
<attribute name="deviceSource" type="int" value="0"/>
</data>
</data>
<data name="tools">
<attribute name="current" type="string" value="text"/>
<data name="drawArrow">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawCoordinateSystem">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawDoubleArrow">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawEllipse">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawRect">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="drawSpline">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="eraser">
<attribute name="drawingType" type="string" value="default"/>
<attribute name="size" type="string" value="MEDIUM"/>
<attribute name="type" type="string" value="default"/>
</data>
<data name="hand">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="highlighter">
<attribute name="color" type="hex" value="ffffff00"/>
<attribute name="drawingType" type="string" value="default"/>
<attribute name="fill" type="int" value="0"/>
<attribute name="fillAlpha" type="int" value="128"/>
<attribute name="size" type="string" value="MEDIUM"/>
</data>
<data name="image">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="pen">
<attribute name="color" type="hex" value="ff3333cc"/>
<attribute name="drawingType" type="string" value="default"/>
<attribute name="fill" type="int" value="0"/>
<attribute name="fillAlpha" type="int" value="128"/>
<attribute name="size" type="string" value="MEDIUM"/>
<attribute name="style" type="string" value="plain"/>
</data>
<data name="playObject">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectMultiLayerRect">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectMultiLayerRegion">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectObject">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectPdfTextLinear">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectPdfTextRect">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectRect">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="selectRegion">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="showFloatingToolbox">
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="text">
<attribute name="color" type="hex" value="ff000000"/>
<attribute name="drawingType" type="string" value="default"/>
</data>
<data name="verticalSpace">
<attribute name="drawingType" type="string" value="default"/>
</data>
</data>
<data name="touch">
<attribute name="cmdDisable" type="string" value=""/>
<attribute name="cmdEnable" type="string" value=""/>
<attribute name="disableTouch" type="boolean" value="false"/>
<attribute name="method" type="string" value="auto"/>
<attribute name="timeout" type="int" value="1000"/>
</data>
</settings>
+5 -23
View File
@@ -2,12 +2,12 @@
name: omarchy name: omarchy
description: > description: >
REQUIRED for end-user customization of Linux desktop, window manager, or system config. REQUIRED for end-user customization of Linux desktop, window manager, or system config.
Use when editing ~/.config/hypr/, ~/.config/omarchy/, Use when editing ANY file in ~/.config/, ~/.config/hypr/, ~/.config/omarchy/,
~/.config/alacritty/, ~/.config/foot/, ~/.config/kitty/, or ~/.config/ghostty/. ~/.config/alacritty/, ~/.config/foot/, ~/.config/kitty/, or ~/.config/ghostty/.
Triggers: Hyprland, window rules, animations, keybindings, monitors, gaps, borders, Triggers: Hyprland, window rules, window behavior, animations, keybindings, monitors, gaps, borders,
blur, opacity, omarchy-shell, bar, terminal config, themes, background, blur, opacity, omarchy-shell, bar, widgets, terminal config, themes, fonts, background,
night light, idle, lock screen, screenshots, reminders, layer rules, workspace night light, idle, lock screen, screenshots, screen recording, reminders, layer rules, workspace
settings, display config, and user-facing omarchy commands. Excludes Omarchy settings, display config, appearance changes, and user-facing omarchy commands. Excludes Omarchy
source development through `omarchy dev link` workflows. source development through `omarchy dev link` workflows.
--- ---
@@ -18,24 +18,6 @@ Manage [Omarchy](https://omarchy.org/) Linux systems - a beautiful, fun, agentic
This skill is for end-user customization on installed systems. This skill is for end-user customization on installed systems.
It is not for contributing to Omarchy source code. It is not for contributing to Omarchy source code.
## When This Skill MUST Be Used
**ALWAYS invoke this skill for end-user requests involving ANY of these:**
- Editing ANY file in `~/.config/hypr/` (window rules, animations, keybindings, monitors, etc.)
- Editing `~/.config/omarchy/shell.json` (status bar layout, widgets)
- Editing terminal configs (alacritty, foot, kitty, ghostty)
- Editing ANY file in `~/.config/omarchy/`
- Window behavior, animations, opacity, blur, gaps, borders
- Layer rules, workspace settings, display/monitor configuration
- Themes, backgrounds, fonts, appearance changes
- User-facing `omarchy` commands (`omarchy theme ...`, `omarchy refresh ...`, `omarchy restart ...`, etc.)
- Screenshots, screen recording, reminders, night light, idle behavior, lock screen
**If you're about to edit a config file in ~/.config/ on this system, STOP and use this skill first.**
**Do NOT use this skill for Omarchy development tasks** (editing the Omarchy source tree, creating migrations, or running `omarchy dev ...` workflows).
## Topic Guides ## Topic Guides
Deeper instructions for common areas live next to this file. Read the Deeper instructions for common areas live next to this file. Read the
+19
View File
@@ -17,6 +17,25 @@ changes. `idle.screensaver` and `idle.lock` are seconds since user idle began.
**Commands:** `omarchy restart shell`, `omarchy refresh shell` **Commands:** `omarchy restart shell`, `omarchy refresh shell`
## Plugin UI Safety
Plugins run inside Omarchy's long-lived, unsandboxed shell. Treat text from network responses, files, IPC, settings, environment variables, and command output as untrusted.
- QML `Text` defaults to `Text.AutoText`. Do not bind untrusted data to an AutoText sink: markup-shaped input such as `<img src="https://example.invalid/tracker">` can be interpreted as rich text and load a resource.
- For display text owned by the plugin, set `textFormat: Text.PlainText`. Prefer a local `PlainText` component when the plugin has multiple text elements.
```qml
Text {
textFormat: Text.PlainText
text: modelData.name
}
```
- When passing dynamic text to an Omarchy-owned component whose internal `Text` format the plugin cannot set, neutralize markup in the plugin first with a helper of its own, and add a regression test; Omarchy ships no such helper. Never edit the packaged component to add a plain-text option: that belongs in a pull request to Omarchy.
- Bound and validate downloaded data before displaying or saving it: limit response and collection sizes, string lengths, identifiers, and timestamps.
- Keep network access explicit. Use fixed HTTPS sources, timeouts, response-size limits, and validation. For periodic background refreshes, disclose the behavior and let users enable or disable it. Never execute downloaded content or interpolate untrusted values into shell commands.
- Add regression coverage for markup-shaped input and every shared tooltip or notification sink that receives dynamic text.
## Bar Layout ## Bar Layout
Use the `omarchy bar` group to move and manage widgets: Use the `omarchy bar` group to move and manage widgets:
@@ -1,6 +1,298 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd"> <!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig> <fontconfig>
<!-- CJK: fold the language tag onto one tag per Noto CJK variant -->
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-hk</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hant</string>
</test>
<edit name="lang" mode="assign">
<string>zh-tw</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-sg</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-mo</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="eq">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-hans</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue-cn</string>
</test>
<edit name="lang" mode="assign">
<string>zh-cn</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>yue</string>
</test>
<edit name="lang" mode="assign">
<string>zh-hk</string>
</edit>
</match>
<!-- Then append that variant after the generic family it falls back from -->
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-cn</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK SC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-hk</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK HK</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<string>zh-tw</string>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK TC</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ja</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK JP</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>sans-serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>serif</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Serif CJK KR</string>
</edit>
</match>
<match target="pattern">
<test name="lang" qual="first" compare="contains">
<langset><string>ko</string></langset>
</test>
<test name="family">
<string>monospace</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Sans Mono CJK KR</string>
</edit>
</match>
<match target="pattern"> <match target="pattern">
<test name="family" qual="any"> <test name="family" qual="any">
<string>sans-serif</string> <string>sans-serif</string>
+7
View File
@@ -0,0 +1,7 @@
/* GTK asks the compositor to blur behind a see-through window only where a
backdrop-filter blur covers it, so Glassy windows load this into GTK apps,
such as Ghostty and Nautilus. The compositor's own blur settings decide
how it looks. */
window {
backdrop-filter: blur(20px);
}
+29
View File
@@ -0,0 +1,29 @@
-- Same terminal tag test as clipboard.lua.
local function active_window_is_terminal()
local window = hl.get_active_window()
if not window then
return false
end
for _, tag in ipairs(window.tags or {}) do
if tag:gsub("%*$", "") == "terminal" then
return true
end
end
return false
end
-- Keep the desktop shortcut independent of Fcitx's custom switching keys.
-- Read the live Fcitx group so setup works without a compositor reload.
o.bind("SUPER + I", "Switch input language", "omarchy-input-method cycle")
o.bind("SUPER + SHIFT + I", "Switch input language back", "omarchy-input-method cycle back")
-- Pass the tmux/Herdr prefix through and return to direct input, so the next
-- key reaches the multiplexer instead of the engine's composition.
o.bind("CTRL + SPACE", "Direct input for the terminal prefix", function()
if active_window_is_terminal() then
-- Wait for the local controller reply before dispatching the next key.
o.shell_succeeds("timeout 0.3 fcitx5-remote --check -c")
end
end, { non_consuming = true })
+43
View File
@@ -0,0 +1,43 @@
-- Defaults for Japanese (JIS) keyboards, which only apply when the installed
-- layout is jp. See default/hypr/keyboard.lua.
local keyboard = require("default.hypr.keyboard")
if not keyboard.jis() then
return
end
-- Same down/up split as the universal clipboard shortcuts in clipboard.lua.
local function send_shortcut_once(mods, key)
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "down" }))
hl.timer(function()
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "up" }))
end, { timeout = 50, type = "oneshot" })
end
local function active_window_class()
local window = hl.get_active_window()
return ((window and window.class) or ""):lower()
end
-- On JIS, + is Shift + ;, so apps see Ctrl + Shift + semicolon and never zoom
-- in. Hand them Ctrl + keypad plus instead, which Chromium, Electron apps,
-- VS Code, and Firefox read as zoom in. Obsidian only listens for Ctrl + ^,
-- which JIS types without Shift.
local zoom_in_overrides = {
obsidian = { "CTRL", "asciicircum" },
}
o.bind("CTRL + SHIFT + semicolon", "Zoom in (JIS + key)", function()
local class = active_window_class()
for pattern, chord in pairs(zoom_in_overrides) do
if class:find(pattern, 1, true) then
send_shortcut_once(chord[1], chord[2])
return
end
end
send_shortcut_once("CTRL", "KP_Add")
end)
+2 -1
View File
@@ -11,7 +11,8 @@ o.bind("SUPER + ALT + F", "Full width", hl.dsp.window.fullscreen({ mode = "maxim
o.bind("SUPER + O", "Pop window out (float & pin)", "omarchy-hyprland-window-pop") o.bind("SUPER + O", "Pop window out (float & pin)", "omarchy-hyprland-window-pop")
o.bind("SUPER + ALT + Home", "Save window width", "omarchy-hyprland-window-width save") o.bind("SUPER + ALT + Home", "Save window width", "omarchy-hyprland-window-width save")
o.bind("SUPER + Home", "Restore window width", "omarchy-hyprland-window-width restore") o.bind("SUPER + Home", "Restore window width", "omarchy-hyprland-window-width restore")
o.bind("SUPER + L", "Toggle workspace layout", "omarchy-hyprland-workspace-layout-toggle") o.bind("SUPER + L", "Cycle workspace layout", "omarchy-hyprland-workspace-layout-toggle")
o.bind("SUPER + ALT + L", "Cycle workspace layout back", "omarchy-hyprland-workspace-layout-toggle back")
o.bind("SUPER + LEFT", "Focus on left window", hl.dsp.focus({ direction = "l" })) o.bind("SUPER + LEFT", "Focus on left window", hl.dsp.focus({ direction = "l" }))
o.bind("SUPER + RIGHT", "Focus on right window", hl.dsp.focus({ direction = "r" })) o.bind("SUPER + RIGHT", "Focus on right window", hl.dsp.focus({ direction = "r" }))
+22
View File
@@ -0,0 +1,22 @@
-- Frosted glass from the glassy Starship (#14726), shared by Glassy windows and
-- the Glassy bar: whatever is behind a see-through surface is blurred.
return function()
hl.config({
decoration = {
blur = {
enabled = true,
size = 4,
passes = 2,
new_optimizations = true,
xray = false,
contrast = 1.0,
brightness = 0.75,
noise = 0.01,
vibrancy = 0.35,
vibrancy_darkness = 0.35,
popups = true,
special = true,
},
},
})
end
+3 -2
View File
@@ -281,7 +281,8 @@ function o.window(match, rules)
hl.window_rule(rules) hl.window_rule(rules)
end end
-- Opt a window in to Omarchy's standard active/inactive transparency. -- Opt a window in to Omarchy's standard active/inactive transparency. The tag
-- lets Appearance > Window Transparency turn these windows opaque or glassy.
function o.transparent_window(match, opacity) function o.transparent_window(match, opacity)
o.window(match, { opacity = opacity or "0.985 0.96" }) o.window(match, { opacity = opacity or "0.985 0.96", tag = "+transparent-window" })
end end
+16 -26
View File
@@ -1,40 +1,19 @@
-- https://wiki.hypr.land/Configuring/Basics/Variables/#input -- https://wiki.hypr.land/Configuring/Basics/Variables/#input
local function read_vconsole() local keyboard = require("default.hypr.keyboard")
local values = {}
local file = io.open("/etc/vconsole.conf", "r")
if not file then
return values
end
for line in file:lines() do
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
if key and value then
value = value:gsub("%s+#.*$", "")
value = value:gsub('^"(.*)"$', "%1")
value = value:gsub("^'(.*)'$", "%1")
values[key] = value
end
end
file:close()
return values
end
-- Layouts that can't type Latin letters. Keep in sync with the list in -- Layouts that can't type Latin letters. Keep in sync with the list in
-- etc/mkinitcpio.conf.d/omarchy_hooks.conf. -- etc/mkinitcpio.conf.d/omarchy_hooks.conf.
local non_latin_layouts = local non_latin_layouts =
" af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua " " af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua "
local vconsole = read_vconsole() local vconsole = keyboard.selected()
local kb_layout = vconsole.XKBLAYOUT or "us" local kb_layout = vconsole.XKBLAYOUT or "us"
local kb_variant = vconsole.XKBVARIANT or "" local kb_variant = vconsole.XKBVARIANT or ""
-- CapsLock is the compose key, so Caps Lock itself has to live somewhere else. -- Caps Lock is Caps Lock, and Shift + Caps Lock is the compose key. Stock XKB
-- Both Shifts together is the usual home for it, but it's easy to hit by -- has no such option, so Omarchy defines it in /etc/xkb.
-- accident while typing. The _cancel variant sets Caps Lock the same way and local kb_options = "omarchy:shift_caps_compose"
-- releases it on the next lone Shift, so a misfire clears itself.
local kb_options = "compose:caps,shift:both_capslock_cancel"
-- Hyprland resolves keybindings against the first entry in kb_layout, not the -- Hyprland resolves keybindings against the first entry in kb_layout, not the
-- layout that's currently active, so Omarchy's Latin-keysym bindings (SUPER + W -- layout that's currently active, so Omarchy's Latin-keysym bindings (SUPER + W
@@ -47,6 +26,17 @@ if non_latin_layouts:find(" " .. kb_layout:match("^[^,]*") .. " ", 1, true) then
kb_options = kb_options .. ",grp:alts_toggle" kb_options = kb_options .. ",grp:alts_toggle"
end end
-- Saved selections already include their Latin-leading layout. Keep the
-- installer's layout-switching shortcut when a non-Latin layout remains.
if not kb_options:find("grp:alts_toggle", 1, true) then
for layout in kb_layout:gmatch("[^,]+") do
if non_latin_layouts:find(" " .. layout .. " ", 1, true) then
kb_options = kb_options .. ",grp:alts_toggle"
break
end
end
end
hl.config({ hl.config({
input = { input = {
kb_layout = kb_layout, kb_layout = kb_layout,
+61
View File
@@ -0,0 +1,61 @@
-- The keyboard layout picked at install time. The installer and first-boot
-- setup persist it to /etc/vconsole.conf, so Hyprland reads it from there
-- instead of rewriting every user's config.
local keyboard = {}
local function read_values(path)
local values = {}
local file = io.open(path, "r")
if not file then
return values
end
for line in file:lines() do
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
if key and value then
value = value:gsub("%s+#.*$", "")
value = value:gsub('^"(.*)"$', "%1")
value = value:gsub("^'(.*)'$", "%1")
values[key] = value
end
end
file:close()
return values
end
function keyboard.selected()
local paths = require("default.hypr.paths")
local values = read_values(paths.config_home .. "/omarchy/keyboard-layouts")
if values.XKBLAYOUT then return values end
return keyboard.installed()
end
function keyboard.vconsole()
return read_values("/etc/vconsole.conf")
end
-- Choices whose console keymap is plain US but whose desktop layout is not:
-- Korean keeps US letters, and Lao is non-Latin, so US still leads.
local desktop_overrides = { kr = true, la = true }
function keyboard.installed()
local values = keyboard.vconsole()
local override = read_values("/etc/omarchy/input-method").XKB_LAYOUT
if desktop_overrides[override]
and (values.XKBLAYOUT or "us") == "us"
and (values.XKBVARIANT or "") == "" then
values.XKBLAYOUT, values.XKBVARIANT = override, ""
end
return values
end
-- Japanese (JIS) keyboards put +, ^, and the Henkan/Muhenkan keys where US
-- keyboards don't, so a few defaults only apply when that layout leads.
function keyboard.jis()
local layout = keyboard.selected().XKBLAYOUT or "us"
return layout:match("^[^,]*") == "jp"
end
return keyboard
+3 -1
View File
@@ -14,7 +14,9 @@ hl.config({
inactive_border = inactive_border_color, inactive_border = inactive_border_color,
}, },
resize_on_border = false, resize_on_border = true,
extend_border_grab_area = 8,
hover_icon_on_border = true,
allow_tearing = false, allow_tearing = false,
layout = "dwindle", layout = "dwindle",
}, },
+2
View File
@@ -8,6 +8,8 @@ require("default.hypr.autostart")
if _G.omarchy_default_bindings ~= false then if _G.omarchy_default_bindings ~= false then
require("default.hypr.bindings.media") require("default.hypr.bindings.media")
require("default.hypr.bindings.clipboard") require("default.hypr.bindings.clipboard")
require("default.hypr.bindings.input-method")
require("default.hypr.bindings.jis")
require("default.hypr.bindings.tiling") require("default.hypr.bindings.tiling")
require("default.hypr.bindings.utilities") require("default.hypr.bindings.utilities")
require("default.hypr.bindings.dictation") require("default.hypr.bindings.dictation")
+61 -8
View File
@@ -1,9 +1,38 @@
local function cursor_theme(value)
if value and value ~= "" and value ~= "omarchy-startup" then return value end
end
local function normal_theme(interrupted)
local current = os.getenv("XCURSOR_THEME")
local theme = cursor_theme(current)
-- Only a lost preference needs recovering; an unset theme still means the default alias.
if theme or not (interrupted or current == "omarchy-startup") then return theme or "default" end
theme = cursor_theme(os.getenv("OMARCHY_STARTUP_CURSOR_THEME"))
if theme then return theme end
local pipe = io.popen("gsettings get org.gnome.desktop.interface cursor-theme 2>/dev/null", "r")
if pipe then
theme = cursor_theme((pipe:read("*a") or ""):match("^'(.-)'%s*$"))
pipe:close()
end
return theme or "default"
end
local function normal_path()
local kept = {}
for entry in (os.getenv("XCURSOR_PATH") or "~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps"):gmatch("[^:]+") do
if not entry:match("/default/hypr/cursors$") then table.insert(kept, entry) end
end
return table.concat(kept, ":")
end
-- Hyprland polls cursor.invisible, so its first frames need a blank cursor too. -- Hyprland polls cursor.invisible, so its first frames need a blank cursor too.
function omarchy_startup_cursor_restore(loaded) function omarchy_startup_cursor_restore(loaded)
if not omarchy_startup_cursor_pending then return end if not omarchy_startup_cursor_pending then return end
local cursor = omarchy_startup_cursor local cursor = omarchy_startup_cursor
if loaded then if loaded then
omarchy_startup_cursor_pending = false omarchy_startup_cursor_pending = false
hl.env("OMARCHY_STARTUP_CURSOR_SESSION", "")
hl.env("OMARCHY_STARTUP_CURSOR_THEME", "")
hl.config({ cursor = cursor.config }) hl.config({ cursor = cursor.config })
if cursor.config.enable_hyprcursor and cursor.hyprcursor then if cursor.config.enable_hyprcursor and cursor.hyprcursor then
hl.exec_cmd("hyprctl setcursor " .. o.shell_quote(cursor.hyprcursor) .. " " .. cursor.size) hl.exec_cmd("hyprctl setcursor " .. o.shell_quote(cursor.hyprcursor) .. " " .. cursor.size)
@@ -11,15 +40,25 @@ function omarchy_startup_cursor_restore(loaded)
elseif not cursor.restoring then elseif not cursor.restoring then
-- Reload the normal Xcursor fallback before enabling Hyprcursor or GSettings. -- Reload the normal Xcursor fallback before enabling Hyprcursor or GSettings.
cursor.restoring = true cursor.restoring = true
hl.env("XCURSOR_PATH", cursor.path)
hl.env("XCURSOR_THEME", cursor.xcursor)
hl.exec_cmd("dbus-update-activation-environment --systemd XCURSOR_PATH XCURSOR_THEME")
local complete = "if omarchy_startup_cursor and omarchy_startup_cursor.xcursor == " .. string.format("%q", cursor.xcursor)
.. " and omarchy_startup_cursor.size == " .. cursor.size .. " then omarchy_startup_cursor_restore(true) end"
hl.exec_cmd("hyprctl setcursor " .. o.shell_quote(cursor.xcursor) .. " " .. cursor.size hl.exec_cmd("hyprctl setcursor " .. o.shell_quote(cursor.xcursor) .. " " .. cursor.size
.. " && hyprctl eval 'omarchy_startup_cursor_restore(true)'") .. " && hyprctl eval " .. o.shell_quote(complete))
end end
end end
hl.on("config.reloaded", function() hl.on("config.reloaded", function()
local recover = false
if omarchy_startup_cursor_pending == nil then if omarchy_startup_cursor_pending == nil then
-- Config updates in an existing compositor must not hide its pointer. -- An ordinary reload must not hide the pointer, but an interrupted reveal survives.
omarchy_startup_cursor_pending = #hl.get_monitors() == 0 local cold = #hl.get_monitors() == 0
local session = os.getenv("HYPRLAND_INSTANCE_SIGNATURE")
local interrupted = session and os.getenv("OMARCHY_STARTUP_CURSOR_SESSION") == session
recover = not cold and not interrupted and os.getenv("XCURSOR_THEME") == "omarchy-startup"
omarchy_startup_cursor_pending = cold or interrupted or recover
if omarchy_startup_cursor_pending then if omarchy_startup_cursor_pending then
local hyprcursor = hl.get_config("cursor.enable_hyprcursor") local hyprcursor = hl.get_config("cursor.enable_hyprcursor")
omarchy_startup_cursor = { omarchy_startup_cursor = {
@@ -30,17 +69,31 @@ hl.on("config.reloaded", function()
}, },
hyprcursor = os.getenv("HYPRCURSOR_THEME"), hyprcursor = os.getenv("HYPRCURSOR_THEME"),
size = tonumber((hyprcursor and os.getenv("HYPRCURSOR_SIZE")) or os.getenv("XCURSOR_SIZE")) or 24, size = tonumber((hyprcursor and os.getenv("HYPRCURSOR_SIZE")) or os.getenv("XCURSOR_SIZE")) or 24,
path = os.getenv("XCURSOR_PATH") or "~/.local/share/icons:~/.icons:/usr/share/icons:/usr/share/pixmaps", path = normal_path(),
xcursor = os.getenv("XCURSOR_THEME") or "default", xcursor = normal_theme(interrupted),
} }
if omarchy_startup_cursor.size <= 0 then omarchy_startup_cursor.size = 24 end if omarchy_startup_cursor.size <= 0 then omarchy_startup_cursor.size = 24 end
hl.env("XCURSOR_PATH", os.getenv("OMARCHY_PATH") .. "/default/hypr/cursors:" .. omarchy_startup_cursor.path) -- Compositor environment survives Lua reloads; a session id prevents stale inheritance.
hl.env("XCURSOR_THEME", "omarchy-startup") hl.env("OMARCHY_STARTUP_CURSOR_THEME", omarchy_startup_cursor.xcursor)
hl.env("OMARCHY_STARTUP_CURSOR_SESSION", os.getenv("HYPRLAND_INSTANCE_SIGNATURE"))
if cold then
hl.env("XCURSOR_PATH", os.getenv("OMARCHY_PATH") .. "/default/hypr/cursors:" .. omarchy_startup_cursor.path)
hl.env("XCURSOR_THEME", "omarchy-startup")
else
hl.env("XCURSOR_PATH", omarchy_startup_cursor.path)
hl.env("XCURSOR_THEME", omarchy_startup_cursor.xcursor)
end
end end
end end
if omarchy_startup_cursor_pending then if omarchy_startup_cursor_pending then
-- Keep the temporary cursor private to the compositor, including GSettings. -- Keep the temporary cursor private to the compositor, including GSettings.
hl.config({ cursor = { invisible = true, enable_hyprcursor = false, sync_gsettings_theme = false } }) hl.config({ cursor = { invisible = true, enable_hyprcursor = false, sync_gsettings_theme = false } })
hl.timer(function() omarchy_startup_cursor_restore() end, { timeout = 15000, type = "oneshot" }) hl.timer(function()
if omarchy_startup_cursor_pending then
omarchy_startup_cursor.restoring = false
omarchy_startup_cursor_restore()
end
end, { timeout = 15000, type = "oneshot" })
if recover then omarchy_startup_cursor_restore() end
end end
end) end)
+9
View File
@@ -0,0 +1,9 @@
-- The Glassy bar: the shell draws the bar see-through, and Hyprland blurs what
-- is behind it.
require("default.hypr.glass")()
hl.layer_rule({
match = { namespace = "^omarchy-bar$" },
blur = true,
ignore_alpha = 0.3,
})
+7
View File
@@ -0,0 +1,7 @@
-- Round window corners, whatever the theme sets. Matches Solitude's corners.
hl.config({
decoration = {
rounding = 6,
rounding_power = 3,
},
})
+6
View File
@@ -0,0 +1,6 @@
-- Square window corners, whatever the theme sets.
hl.config({
decoration = {
rounding = 0,
},
})
+2 -4
View File
@@ -1,6 +1,6 @@
-- Draw nothing just for show, for machines that render on the CPU, like a VM: -- Draw nothing just for show, for machines that render on the CPU, like a VM:
-- no animations, blur or shadows, and opaque windows, so nothing behind a -- no animations, blur or shadows. How see-through windows are is left to
-- window has to be blended into it. -- Appearance > Window Transparency.
hl.config({ hl.config({
animations = { animations = {
enabled = false, enabled = false,
@@ -16,5 +16,3 @@ hl.config({
}, },
}, },
}) })
o.window(".*", { opacity = "1 1" })
+14
View File
@@ -0,0 +1,14 @@
-- Frosted glass from the glassy Starship (#14726): whatever is behind a
-- see-through window or shell surface is blurred. Terminals get the see-through
-- background itself from omarchy-theme-set-glass, which keeps their text crisp.
require("default.hypr.glass")()
-- GTK apps fade as a whole, since GTK draws their backgrounds solid, and get
-- their blur from the backdrop-filter Glassy adds to the user's gtk.css.
o.window("org\\.gnome\\.Nautilus", { opacity = "0.75 0.75" })
hl.layer_rule({
match = { namespace = "^(omarchy-bar|omarchy-menu|omarchy-notifications|omarchy-osd|omarchy-polkit|omarchy-reminders|omarchy-clipboard|omarchy-emojis|omarchy-image-selector|omarchy-keyboard-panel|omarchy-network-qr)$" },
blur = true,
ignore_alpha = 0.3,
})
+2
View File
@@ -0,0 +1,2 @@
-- Opaque windows, even those Omarchy makes transparent by default.
o.window({ tag = "transparent-window" }, { opacity = "1 1" })
+13
View File
@@ -3,6 +3,19 @@
local paths = require("default.hypr.paths") local paths = require("default.hypr.paths")
local require_all = require("default.hypr.require_all") local require_all = require("default.hypr.require_all")
-- The saved files call o.workspace_mode, so it has to exist before they load.
local workspace_modes = require("default.hypr.workspace-modes")
local layouts_dir = paths.state_home .. "/omarchy/workspace-layouts" local layouts_dir = paths.state_home .. "/omarchy/workspace-layouts"
-- The directory is found through XDG_STATE_HOME but the files are loaded by
-- module name, and bootstrap only puts ~/.local/state on the search path. A
-- state home anywhere else would list the saved layouts and then fail to require
-- them, or load same-named ones from under the home directory instead.
package.path = paths.state_home .. "/?.lua;" .. package.path
require_all.files(layouts_dir, "omarchy.workspace-layouts", { reload = true }) require_all.files(layouts_dir, "omarchy.workspace-layouts", { reload = true })
-- With every saved mode known, bring the open windows in line and set up what a
-- floating workspace needs.
workspace_modes.apply()
+385
View File
@@ -0,0 +1,385 @@
-- Workspace modes for omarchy-hyprland-workspace-layout-toggle.
--
-- Hyprland has dwindle and scrolling as tiled layouts, but nothing that floats a
-- whole workspace: it reports a workspace's `tiledLayout` and takes an unknown
-- layout name in silence, falling back to dwindle. So floating is applied to the
-- windows themselves.
--
-- Ending the mode has to tile the windows the mode floated and only those, so
-- each one is tagged. The tag lives on the window: it dies with it, follows it
-- between workspaces, and survives the config reload that a monitor being
-- plugged in performs, none of which a table on this side would do.
--
-- Every window on a floating workspace also carries a second tag, whether the
-- mode floated it or it was floating already. Titlebars, shadows, and full
-- opacity follow that tag, so a dialog on a floating workspace can be dragged by
-- its titlebar too, and a window carried out to a tiled one loses it.
require("default.hypr.helpers")
local paths = require("default.hypr.paths")
local FLOATED_TAG = "omarchy-mode-floated"
local WORKSPACE_TAG = "omarchy-floating-workspace"
local TITLEBARS = "/usr/lib/omarchy-hyprland-titlebars/titlebars.so"
local M = {}
local modes = {}
local default_mode = nil
local watching = false
local cascade = 0
local function set_floating(window, floating)
hl.dispatch(hl.dsp.window.float({ action = floating and "on" or "off", window = window }))
end
local function set_claimed(window, claimed)
hl.dispatch(hl.dsp.window.tag({ tag = (claimed and "+" or "-") .. FLOATED_TAG, window = window }))
end
local function set_on_floating_workspace(window, on)
hl.dispatch(hl.dsp.window.tag({ tag = (on and "+" or "-") .. WORKSPACE_TAG, window = window }))
end
local function claimed()
local addresses = {}
for _, window in ipairs(hl.get_windows({ tag = FLOATED_TAG })) do
addresses[window.address] = true
end
return addresses
end
-- Special workspaces, such as the scratchpad, and named ones, which report a
-- negative id, keep whatever state a window arrived with.
local function floating_workspace(id)
id = tonumber(id)
if not id or id < 1 then
return false
end
local mode = modes[tostring(id)]
if mode then
return mode == "floating"
end
return default_mode == "floating"
end
local function anything_floats()
if default_mode == "floating" then
return true
end
for _, mode in pairs(modes) do
if mode == "floating" then
return true
end
end
return false
end
-- A window the mode floats opens at a comfortable size in the middle of its
-- monitor, each one stepped from the last so a workspace floated all at once does
-- not leave every window stacked exactly on the one beneath it.
local function place(window)
local monitor = window.monitor
if not monitor or not monitor.width then
return
end
local width = math.floor(monitor.width / monitor.scale)
local height = math.floor(monitor.height / monitor.scale)
if (monitor.transform or 0) % 2 == 1 then
width, height = height, width
end
local reserved = monitor.reserved or {}
width = width - (reserved.left or 0) - (reserved.right or 0)
height = height - (reserved.top or 0) - (reserved.bottom or 0)
hl.dispatch(hl.dsp.window.resize({
x = math.min(1100, math.floor(width * 0.64)),
y = math.min(760, math.floor(height * 0.68)),
relative = false,
window = window,
}))
hl.dispatch(hl.dsp.window.center({ window = window }))
cascade = (cascade + 1) % 5
hl.dispatch(hl.dsp.window.move({ x = cascade * 24 - 48, y = cascade * 24 - 48, relative = true, window = window }))
end
-- A window already floating on its own account -- an app with a float rule of
-- its own, a dialog, one popped out with SUPER + T -- is left unclaimed, so it
-- is still floating when the mode ends. A fullscreen window is left as it is.
--
-- Every tiled window is claimed before any of them is floated: floating one
-- member of a group floats the rest, and a single pass would then find those
-- already floating and leave them to be stranded when the mode ends.
local function float_for_mode(windows)
local claiming = {}
for _, window in ipairs(windows) do
set_on_floating_workspace(window, true)
if not window.floating and (window.fullscreen or 0) == 0 then
set_claimed(window, true)
table.insert(claiming, window)
end
end
for _, window in ipairs(claiming) do
set_floating(window, true)
place(window)
end
end
-- A pinned window is one the user asked to keep above everything, and tiling it
-- would unpin it as well, so the mode leaves it be.
local function tile_for_mode(window, mine)
set_on_floating_workspace(window, false)
if not mine[window.address] or window.pinned then
return
end
set_claimed(window, false)
set_floating(window, false)
end
-- The workspace is passed in when a window is still on its way to it.
local function arrange(windows, workspace)
local floating = {}
local mine = claimed()
for _, window in ipairs(windows) do
local id = tonumber(workspace and workspace.id or (window.workspace and window.workspace.id))
if id and id >= 1 then
if floating_workspace(id) then
table.insert(floating, window)
else
tile_for_mode(window, mine)
end
end
end
float_for_mode(floating)
end
-- Overlapping windows are placed with the pointer, so a floating workspace
-- focuses on click rather than on hover. Every other workspace keeps the user's
-- own setting.
local input = {}
local function update_input()
local workspace = hl.get_active_workspace()
local active = workspace ~= nil and floating_workspace(workspace.id)
if active == input.floating then
return
end
input.floating = active
hl.config({ input = { follow_mouse = active and 0 or input.follow_mouse } })
end
-- Windows arrive by opening and by being carried in, and a window rule only ever
-- fires for the first. One carried back out gives up the floating the mode gave
-- it rather than staying floating somewhere that tiles.
local function watch()
if watching then
return
end
watching = true
input.follow_mouse = hl.get_config("input.follow_mouse")
hl.on("window.open", function(window)
arrange({ window })
end)
-- A move is still being committed when this fires, and changing the window's
-- floating state underneath it loses the move. Let it land first.
hl.on("window.move_to_workspace", function(window, workspace)
hl.timer(function()
arrange({ window }, workspace)
end, { type = "oneshot", timeout = 1 })
end)
-- A fullscreen window is left as it is, so it is arranged once it comes back
-- out: a tiled one leaving fullscreen on a floating workspace floats then.
hl.on("window.fullscreen", function(window)
hl.timer(function()
arrange({ window })
end, { type = "oneshot", timeout = 1 })
end)
hl.on("workspace.active", update_input)
hl.on("monitor.focused", update_input)
end
function o.workspace_mode(spec)
watch()
-- The default for every workspace that has no mode of its own, chosen under
-- Trigger > Appearance > Workspace Layout. A tiled default is Hyprland's own
-- layout; a floating one is arranged once the saved modes have all loaded.
if spec.default then
default_mode = spec.default
if spec.default ~= "floating" then
hl.config({ general = { layout = spec.default } })
end
return
end
local workspace = tostring(spec.workspace)
-- Floating keeps whatever tiled layout the workspace had, so nothing has to be
-- chosen for it on the way back out.
if spec.mode ~= "floating" then
hl.workspace_rule({ workspace = workspace, layout = spec.mode })
end
modes[workspace] = spec.mode
arrange(hl.get_workspace_windows(workspace))
update_input()
end
local function theme_colors()
local colors = {}
-- omarchy-theme-set writes the current theme under ~/.local/state whatever
-- XDG_STATE_HOME says, and Hyprland loads the theme's own config from there.
local file = io.open(paths.home .. "/.local/state/omarchy/current/theme/colors.toml", "r")
if file then
for line in file:lines() do
local key, value = line:match('^%s*([%w_]+)%s*=%s*"#(%x%x%x%x%x%x)"')
if key then
colors[key] = "rgb(" .. value .. ")"
end
end
file:close()
end
local background = colors.background or "rgb(1a1b26)"
-- A titlebar in the window's own background color runs into the window, so it
-- takes the theme's raised surface. Themes that make that the background too
-- fall back to their selection color.
local titlebar = colors.lighter_background
if not titlebar or titlebar == background then
titlebar = colors.selection or background
end
return {
background = background,
foreground = colors.foreground or "rgb(c0caf5)",
titlebar = titlebar,
}
end
-- Titlebars come from omarchy-hyprland-titlebars, loaded only while something
-- floats. Hyprland keeps a plugin loaded after the config stops asking for it,
-- but resets its options, and the plugin draws nothing until enabled here.
-- The plugin reloads the config once it is in, which is the pass that finds its
-- options here.
local function titlebars()
local file = io.open(TITLEBARS, "r")
if not file then
return
end
file:close()
hl.plugin.load(TITLEBARS)
if not hl.plugin.hyprbars then
return
end
local colors = theme_colors()
local window = [[window = "address:%WINDOW%"]]
hl.config({
plugin = {
hyprbars = {
enabled = true,
workspace_tag = WORKSPACE_TAG,
bar_height = 30,
bar_color = colors.titlebar,
col = { text = colors.foreground },
bar_text_font = "monospace",
bar_text_size = 12,
bar_text_weight = "normal",
bar_text_align = "left",
bar_buttons_alignment = "right",
bar_part_of_window = true,
bar_precedence_over_border = true,
bar_padding = 10,
bar_button_padding = 8,
button_rounding = 0,
icon_on_hover = false,
inactive_button_color = colors.titlebar,
on_double_click = "hyprctl dispatch 'hl.dsp.window.fullscreen({ mode = \"maximized\", action = \"toggle\", " .. window .. " })'",
},
},
})
for _, button in ipairs({
{ icon = "×", action = "hyprctl dispatch 'hl.dsp.window.close({ " .. window .. " })'" },
{ icon = "□", action = "hyprctl dispatch 'hl.dsp.window.fullscreen({ mode = \"maximized\", action = \"toggle\", " .. window .. " })'" },
}) do
hl.plugin.hyprbars.add_button({
bg_color = colors.titlebar,
fg_color = colors.foreground,
size = 20,
icon = button.icon,
action = button.action,
})
end
end
-- Runs once every saved mode has loaded. A reload is a fresh start on this side
-- but not for the windows, so each one is brought in line with the modes as they
-- now stand: moving the default away from floating has to give back what it
-- floated, even though nothing names those workspaces any more.
function M.apply()
-- A default that no longer floats can leave no saved mode at all to start the
-- watchers, while the windows it floated still carry its tags.
local leftovers = #hl.get_windows({ tag = FLOATED_TAG }) > 0 or #hl.get_windows({ tag = WORKSPACE_TAG }) > 0
if anything_floats() or leftovers then
watch()
end
if watching then
arrange(hl.get_windows())
update_input()
end
if not anything_floats() then
return
end
-- Overlapping windows need depth to tell apart, and a translucent window over
-- another is hard to read.
if not hl.get_config("decoration.shadow.enabled") then
hl.config({ decoration = { shadow = { enabled = true, range = 16, render_power = 3, color = "rgba(00000035)" } } })
o.window(".*", { no_shadow = true })
o.window({ tag = WORKSPACE_TAG }, { no_shadow = false })
end
o.window({ tag = WORKSPACE_TAG }, { opacity = "1.0 override 1.0 override" })
titlebars()
end
return M
+332
View File
@@ -0,0 +1,332 @@
"""Fcitx defaults for offline finalization and updates."""
import argparse
from contextlib import contextmanager
import json
import os
from pathlib import Path
import re
import subprocess
import tempfile
import sys
import time
ROOT = Path(os.environ["OMARCHY_PATH"])
PRESETS = json.loads((ROOT / "default/input-methods/presets.json").read_text())
NON_LATIN = set("af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua".split())
CONTROLLER = ["busctl", "--user", "call", "org.fcitx.Fcitx5", "/controller", "org.fcitx.Fcitx.Controller1"]
def run(args, **kwargs):
return subprocess.run(args, check=True, text=True, capture_output=True, **kwargs).stdout.strip()
def read(path):
return path.read_text() if path.exists() else ""
def values(text):
result = {}
for line in text.splitlines():
match = re.match(r"^\s*([\w ]+)\s*=\s*(.*?)\s*(?:\s+#.*)?$", line)
if match:
result[match[1].strip()] = match[2].strip("\"'")
return result
def sections(text):
result = {}
current = ""
for line in text.splitlines():
match = re.match(r"^\s*\[([^]]+)\]\s*$", line)
if match:
current = match[1]
result.setdefault(current, {})
elif current:
result[current].update(values(line))
return result
def atomic_write(path, text):
# Follow a user's config symlink rather than replacing the link itself.
path = path.resolve()
if read(path) == text:
return False
path.parent.mkdir(parents=True, exist_ok=True)
mode = path.stat().st_mode & 0o777 if path.exists() else 0o644
fd, temporary = tempfile.mkstemp(prefix=".omarchy-input-", dir=path.parent)
try:
with os.fdopen(fd, "w") as file:
file.write(text)
os.chmod(temporary, mode)
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return True
def key_list(entries):
# Fcitx canonicalizes modifiers (e.g. Shift+Super instead of Super+Shift).
return {frozenset(value.split("+")) for value in entries.values()}
def without_ctrl_space(text):
# Ctrl+Space belongs to tmux and Herdr, and the terminal binding returns to
# direct input on it; a Fcitx toggle on the same key would undo that. Without
# a TriggerKeys list Fcitx falls back to its default, which has Ctrl+Space.
section = re.search(r"(?ms)^\[Hotkey/TriggerKeys\][ \t]*\n(.*?)(?=^\[|\Z)", text)
if not section:
return text.rstrip() + ("\n\n" if text.strip() else "") + "[Hotkey/TriggerKeys]\n0=Zenkaku_Hankaku\n1=Hangul\n"
keys = values(section[1])
kept = [keys[key] for key in sorted(keys, key=lambda key: int(key) if key.isdigit() else 0) if keys[key] != "Control+space"]
if len(kept) == len(keys):
return text
body = "".join(f"{index}={key}\n" for index, key in enumerate(kept)) + ("\n" if section[1].endswith("\n\n") else "")
return text[:section.start(1)] + body + text[section.end(1):]
def defaults(config_home, fresh=False):
changed = False
files = ["config", "conf/quickphrase.conf", "conf/wayland.conf", "conf/xcb.conf", "conf/pinyin.conf"]
for name in files:
target = config_home / "fcitx5" / name
original = read(target)
shipped = read(ROOT / "config/fcitx5" / name)
if name == "config" or name == "conf/quickphrase.conf":
if name == "config":
original = without_ctrl_space(original)
existing = sections(original)
additions = []
for section, entries in sections(shipped).items():
# Missing hotkey sections also represent a choice: Fcitx's defaults.
if not fresh and section.startswith("Hotkey/"):
continue
# Existing lists (including explicitly empty lists) are user choices.
# A fresh profile has no working IME shortcut to preserve.
stock_lists = {
"Hotkey/ActivateKeys": {"0": "Hangul_Hanja"},
"Hotkey/DeactivateKeys": {"0": "Hangul_Romaja"},
"Hotkey/EnumerateGroupForwardKeys": {"0": "Super+space"},
"Hotkey/EnumerateGroupBackwardKeys": {"0": "Super+Shift+space"},
"TriggerKey": {"0": "Super+grave", "1": "Super+semicolon"},
}
if fresh and section in existing and section in stock_lists and (key_list(existing[section]) == key_list(stock_lists[section]) or (not existing[section] and section in {"Hotkey/ActivateKeys", "Hotkey/DeactivateKeys"})):
original = re.sub(r"(?ms)^\[" + re.escape(section) + r"\]\s*\n.*?(?=^\[|\Z)", "", original)
existing.pop(section)
if section not in existing:
additions.append(f"[{section}]\n" + "".join(f"{key}={value}\n" for key, value in entries.items()))
if additions:
original = original.rstrip() + ("\n\n" if original.strip() else "") + "\n".join(additions)
changed |= atomic_write(target, original)
elif name == "conf/pinyin.conf":
# Suppress the first-use cloud prompt while preserving any explicit
# cloud-prediction preference and all other existing engine settings.
if not original:
updated = shipped
elif "FirstRun" in values(original):
updated = re.sub(r"(?m)^\s*FirstRun\s*=.*$", "FirstRun=False", original)
else:
updated = "FirstRun=False\n" + original
changed |= atomic_write(target, updated)
elif not target.exists() and not target.is_symlink():
changed |= atomic_write(target, shipped)
return changed
def activation_default():
data_home = Path(os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share"))
path = data_home / "dbus-1/services/org.fcitx.Fcitx5.service"
original = read(path)
# Repair the package's plain activation entry, but retain custom launchers.
packaged = values(original).get("Exec") in ("/usr/bin/fcitx5", "/usr/bin/fcitx5 -d")
if not original or (packaged and "SystemdService" not in values(original)):
return atomic_write(path, read(ROOT / "default/input-methods/org.fcitx.Fcitx5.service"))
return False
@contextmanager
def transaction(config_home):
data_home = Path(os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local/share"))
paths = [config_home / "fcitx5" / name for name in ["profile", "config", "conf/quickphrase.conf", "conf/wayland.conf", "conf/xcb.conf", "conf/pinyin.conf"]]
paths += [config_home / "fontconfig/conf.d/50-omarchy-input-method.conf", data_home / "dbus-1/services/org.fcitx.Fcitx5.service"]
before = {path.resolve(): path.read_text() if path.exists() else None for path in paths}
try:
yield
except BaseException:
for path, original in before.items():
if original is None:
path.unlink(missing_ok=True)
else:
atomic_write(path, original)
raise
def selection():
vconsole = values(read(Path(os.environ.get("OMARCHY_VCONSOLE", "/etc/vconsole.conf"))))
preference = values(read(Path(os.environ.get("OMARCHY_INPUT_SELECTION", "/etc/omarchy/input-method"))))
layout = vconsole.get("XKBLAYOUT", "us").split(",")[0] or "us"
variant = vconsole.get("XKBVARIANT", "").split(",")[0]
if preference.get("XKB_LAYOUT") in ("kr", "la") and vconsole.get("XKBLAYOUT", "us") == "us" and not variant:
layout, variant = preference["XKB_LAYOUT"], ""
method = preference.get("INPUT_METHOD", "mozc" if layout == "jp" else "none")
if method not in PRESETS:
raise ValueError(f"Unknown installed input method: {method}")
if layout in NON_LATIN:
layout, variant = "us", ""
group_layout = layout + (f"-{variant}" if variant else "")
if not re.fullmatch(r"[A-Za-z0-9_-]+", group_layout):
raise ValueError("Invalid installed keyboard layout")
return method, group_layout
def stock_profile(text):
if not text.strip():
return True
groups = sections(text)
items = [data.get("Name", "") for key, data in groups.items() if re.fullmatch(r"Groups/\d+/Items/\d+", key)]
group = groups.get("Groups/0", {})
return (items == ["keyboard-us"]
and list(key for key in groups if re.fullmatch(r"Groups/\d+", key)) == ["Groups/0"]
and group.get("Name", "Default") == "Default"
and group.get("Default Layout", "us") == "us"
and not groups.get("Groups/0/Items/0", {}).get("Layout"))
def profile_text(layout, method):
default_im = method if method != "none" else f"keyboard-{layout}"
text = f"[Groups/0]\nName=Default\nDefault Layout={layout}\nDefaultIM={default_im}\n\n[Groups/0/Items/0]\nName=keyboard-{layout}\nLayout=\n"
if method != "none":
text += f"\n[Groups/0/Items/1]\nName={method}\nLayout=\n"
return text + "\n[GroupOrder]\n0=Default\n"
def font_default(config_home, method):
variant = PRESETS[method]["font"]
path = config_home / "fontconfig/conf.d/50-omarchy-input-method.conf"
if not variant or path.exists() or path.is_symlink():
return
# Only CJK fallback changes; retain the interface and terminal's Latin font.
rules = []
for families, face in [("sans-serif|Liberation Sans", "Sans"), ("serif|Liberation Serif", "Serif"), ("monospace|JetBrainsMono Nerd Font", "Sans Mono")]:
for family in families.split("|"):
rules.append(f'<alias><family>{family}</family><accept><family>Noto {face} CJK {variant}</family></accept></alias>')
atomic_write(path, '<?xml version="1.0"?>\n<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">\n<fontconfig>\n' + "\n".join(rules) + "\n</fontconfig>\n")
def live_group():
group = run(["fcitx5-remote", "-q"])
info = json.loads(run(CONTROLLER[:1] + ["--json=short"] + CONTROLLER[1:] + ["InputMethodGroupInfo", "s", group]))["data"]
layout, items = info[:2]
return group, layout, items
def live_set(group, layout, items):
args = [group, layout, str(len(items))]
for name, item_layout in items:
args.extend([name, item_layout])
run(CONTROLLER + ["SetInputMethodGroupInfo", "ssa(ss)"] + args)
run(CONTROLLER + ["Save"])
def reload_bus():
# Offline finalization and updates over SSH may have no session bus.
try:
run(["busctl", "--user", "call", "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "ReloadConfig"])
except (OSError, subprocess.CalledProcessError, RuntimeError) as error:
print(f"Input activation reload deferred until login: {error}", file=sys.stderr)
def available_methods():
return json.loads(run(CONTROLLER[:1] + ["--json=short"] + CONTROLLER[1:] + ["AvailableInputMethods"]))["data"][0]
def wait_ready(method):
for attempt in range(100):
try:
available = available_methods()
if any(item[0] == method for item in available) and live_group()[2]:
return
except (OSError, ValueError, KeyError, IndexError, subprocess.CalledProcessError):
pass
time.sleep(0.1)
raise RuntimeError(f"Fcitx did not load the {method} engine after restart")
def configure(args):
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config"))
running = subprocess.run(["pgrep", "-u", str(os.getuid()), "-x", "fcitx5"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
# Finalization can be retried after login. A live daemon owns its profile.
if args.seed and running:
return
with transaction(config_home):
configure_inner(args, config_home, running)
def configure_inner(args, config_home, running):
method, layout = selection()
profile = config_home / "fcitx5/profile"
original = read(profile)
if args.seed:
if not stock_profile(original):
return
atomic_write(profile, profile_text(layout, method))
defaults(config_home, fresh=True)
activation_default()
font_default(config_home, method)
return
fresh = stock_profile(original)
changed = defaults(config_home, fresh=fresh)
if activation_default():
reload_bus()
# Upgrades can run through sudo/SSH without access to the live user's bus.
# Never overwrite its profile or restart input in that context.
if fresh:
if running:
try:
group, old_layout, items = live_group()
if old_layout == "us" and items == [["keyboard-us", ""]]:
new_items = [[f"keyboard-{layout}", ""]]
if method != "none":
if any(item[0] == method for item in available_methods()):
new_items.append([method, ""])
else:
print(f"Finish input setup after login: omarchy setup input {method}", file=sys.stderr)
live_set(group, layout, new_items)
if live_group()[1:] != (layout, new_items):
live_set(group, old_layout, items)
raise RuntimeError("Fcitx did not retain the installed keyboard settings")
except (OSError, ValueError, KeyError, IndexError, RuntimeError, subprocess.CalledProcessError) as error:
print(f"Input profile alignment deferred until login: {error}", file=sys.stderr)
else:
atomic_write(profile, profile_text(layout, method))
if running and changed:
try:
run(CONTROLLER + ["ReloadConfig"])
except (OSError, subprocess.CalledProcessError) as error:
print(f"Input defaults reload deferred until login: {error}", file=sys.stderr)
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("status")
configure_parser = commands.add_parser("configure")
mode = configure_parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--seed", action="store_true")
mode.add_argument("--defaults", action="store_true")
args = parser.parse_args()
if args.command == "status":
method, layout = selection()
print(json.dumps({"installed_method": method, "layout": layout}))
else:
configure(args)
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
raise SystemExit(f"Input method setup failed: {error}")
+189
View File
@@ -0,0 +1,189 @@
"""Live Fcitx state for the existing keyboard layout widget."""
from collections import Counter
import json
import os
import re
import subprocess
import sys
from gi.repository import Gio, GLib
BUS = "org.fcitx.Fcitx5"
INTERFACE = "org.fcitx.Fcitx.Controller1"
def call(bus, method, args=None):
# Reading the bar must neither activate a stopped service nor wait indefinitely.
return bus.call_sync(BUS, "/controller", INTERFACE, method, args, None,
Gio.DBusCallFlags.NO_AUTO_START, 1000, None).unpack()
def snapshot(bus):
group = call(bus, "CurrentInputMethodGroup")[0]
methods = list(dict.fromkeys(item[0] for item in
call(bus, "InputMethodGroupInfo", GLib.Variant("(s)", (group,)))[1]))
info = call(bus, "CurrentInputMethodInfo")
return {"methods": methods, "current": info[0], "name": info[1], "label": info[4], "language": info[5]}
def layout_keyboard(keyboards):
typed = [keyboard for keyboard in keyboards if not re.match(
r"^(hl-virtual-keyboard|power-button|sleep-button|lid-switch|video-bus)", keyboard.get("name", ""))]
typed = [item for item in typed if len(item.get("layout", "").split(",")) > 1]
if not typed:
return None
# Consumer controls also carry the seat layout. Its frequency keeps an
# individually configured keyboard from taking over the desktop shortcut.
counts = Counter(item.get("layout", "") for item in keyboards)
layout = max((item["layout"] for item in typed), key=lambda layout: counts[layout])
keyboard = max((item for item in typed if item["layout"] == layout),
key=lambda item: item.get("active_layout_index", 0))
return keyboard
def layout_switches(keyboards, index=None, step=1):
keyboard = layout_keyboard(keyboards)
if keyboard is None:
return []
layout = keyboard["layout"]
following = index if index is not None else (keyboard.get("active_layout_index", 0) + step) % len(layout.split(","))
return [["hyprctl", "switchxkblayout", item["name"], str(following)]
for item in keyboards if item.get("layout") == layout and item.get("name")]
def cycle_choice(state, keyboards, step=1):
methods = state.get("methods", [])
keyboard = layout_keyboard(keyboards)
primary = next((method for method in methods if method.startswith("keyboard-")), None)
if keyboard is None or primary is None:
current = state.get("current")
if len(methods) > 1 and current in methods:
return methods[(methods.index(current) + step) % len(methods)], None
return None, None
# One direct-input method represents all compositor layouts. Composition
# engines follow them in the user's configured order.
choices = []
for method in methods:
if method == primary:
choices.extend((method, index) for index in range(len(keyboard["layout"].split(","))))
else:
choices.append((method, None))
current = state.get("current") or primary
position = (current, keyboard.get("active_layout_index", 0) if current == primary else None)
if position not in choices:
return None, None
return choices[(choices.index(position) + step) % len(choices)]
class Indicator:
def __init__(self, bus):
self.bus = bus
self.pending = ""
self.last = ""
self.entries = {}
def select_next(self, step=1):
devices = subprocess.run(["hyprctl", "-j", "devices"], check=True, text=True, capture_output=True)
keyboards = json.loads(devices.stdout).get("keyboards", [])
try:
state = snapshot(self.bus)
except GLib.Error:
# Without Fcitx, Super+I still cycles the compositor layouts.
for command in layout_switches(keyboards, step=step):
subprocess.run(command, check=True, capture_output=True)
return
methods = state["methods"]
current = self.pending or state["current"] or self.last
state["current"] = current if current in methods else next(iter(methods), "")
following, index = cycle_choice(state, keyboards, step)
if following is None:
return
self.pending = following
for command in layout_switches(keyboards, index) if index is not None else []:
subprocess.run(command, check=True, capture_output=True)
self.refresh()
def refresh(self):
state = snapshot(self.bus)
methods = state["methods"]
if self.pending not in methods:
self.pending = ""
if self.pending and state["current"]:
# Before the first application gains focus Fcitx has no input context.
# Retain the click until it has one, without changing startup defaults.
call(self.bus, "SetCurrentIM", GLib.Variant("(s)", (self.pending,)))
state = snapshot(self.bus)
if state["current"] == self.pending:
self.pending = ""
if state["current"]:
self.last = state["current"]
else:
chosen = self.pending or (self.last if self.last in methods else "") or next(iter(methods), "")
if chosen not in self.entries:
self.entries = {entry[0]: entry for entry in call(self.bus, "AvailableInputMethods")[0]}
entry = self.entries.get(chosen)
if entry:
state.update(current=entry[0], name=entry[1], label=entry[4], language=entry[5])
return state
def watch(bus):
previous = None
indicator = Indicator(bus)
loop = GLib.MainLoop()
buffer = ""
timer = 0
def refresh():
nonlocal previous
try:
state = indicator.refresh()
except GLib.Error:
indicator.pending = ""
indicator.last = ""
state = {"methods": [], "current": "", "name": "", "language": ""}
line = json.dumps(state, ensure_ascii=False)
if line != previous:
print(line, flush=True)
previous = line
def poll():
nonlocal timer
refresh()
timer = GLib.timeout_add(50 if indicator.pending else 500, poll)
return GLib.SOURCE_REMOVE
def command(source, condition):
nonlocal buffer, timer
data = os.read(sys.stdin.fileno(), 4096)
if not data:
loop.quit()
return GLib.SOURCE_REMOVE
buffer += data.decode()
while "\n" in buffer:
line, buffer = buffer.split("\n", 1)
if line in ("cycle", "cycle back"):
try:
indicator.select_next(-1 if line == "cycle back" else 1)
except (GLib.Error, OSError, ValueError, subprocess.CalledProcessError):
indicator.pending = ""
refresh()
GLib.source_remove(timer)
timer = GLib.timeout_add(50 if indicator.pending else 500, poll)
return GLib.SOURCE_CONTINUE
# Fcitx's controller has no current-method-changed signal. Keep one bus
# connection open instead of spawning a command on every poll. Polling also
# follows per-application input contexts and recovers after service restarts.
GLib.io_add_watch(sys.stdin, GLib.IO_IN | GLib.IO_HUP, command)
poll()
loop.run()
if __name__ == "__main__":
try:
watch(Gio.bus_get_sync(Gio.BusType.SESSION, None))
except (GLib.Error, OSError, ValueError, subprocess.CalledProcessError) as error:
raise SystemExit(str(error))
@@ -0,0 +1,5 @@
# Installed per-user so D-Bus activation uses the supervised Omarchy unit.
[D-BUS Service]
Name=org.fcitx.Fcitx5
Exec=/usr/bin/fcitx5
SystemdService=omarchy-fcitx5.service
+7
View File
@@ -0,0 +1,7 @@
{
"none": {"label": "Keyboard only", "packages": [], "font": ""},
"mozc": {"label": "Japanese (Mozc)", "packages": ["fcitx5-mozc"], "font": "JP"},
"hangul": {"label": "Korean (Hangul)", "packages": ["fcitx5-hangul"], "font": "KR"},
"pinyin": {"label": "Chinese (Simplified, Pinyin)", "packages": ["fcitx5-chinese-addons"], "font": "SC"},
"chewing": {"label": "Chinese (Traditional, Zhuyin)", "packages": ["fcitx5-chewing"], "font": "TC"}
}
+251
View File
@@ -0,0 +1,251 @@
"""Native menu selectors for keyboard layouts and active input methods."""
from collections import Counter
import json
import os
from pathlib import Path
import subprocess
import sys
import configure as setup
def keyboard_catalog():
# Read the same choices as the ISO and first-boot form, then convert console
# keymaps through systemd's mapping, just as localectl does during install.
choices = setup.run(["bash", "-c", 'source "$1"; printf "%s\\n" "$OMARCHY_KEYBOARD_LAYOUTS"',
"bash", str(setup.ROOT / "install/provisioning/setup-form.sh")])
mappings = {}
for line in Path("/usr/share/systemd/kbd-model-map").read_text().splitlines():
fields = line.split()
if not fields or fields[0].startswith("#"):
continue
layout = fields[1].split(",")[0]
variant = fields[3].split(",")[0]
mappings.setdefault(fields[0], layout + (":" + variant if variant != "-" else ""))
# The console names systemd's table lacks convert as keyboard_xkb_settings in
# omarchy-provision-owner does, so a choice types what it did at install.
mappings.update({"bg-cp1251": "bg:phonetic", "colemak": "us:colemak", "cz": "cz:qwerty",
"kyrgyz": "kg", "de_CH-latin1": "ch", "no-latin1": "no"})
catalog = {}
for choice in choices.splitlines():
label, keymap, *input_settings = choice.split("|")
layout = input_settings[1] if len(input_settings) > 1 else mappings.get(keymap, keymap)
# Composition engines belong in Input Methods, not as duplicate US layouts.
catalog.setdefault(layout, label)
return catalog
def keyboard_selection():
layouts = json.loads(setup.run(["hyprctl", "-j", "getoption", "input:kb_layout"]))["str"].split(",")
variants = json.loads(setup.run(["hyprctl", "-j", "getoption", "input:kb_variant"]))["str"].split(",")
return [layout + (":" + variants[index] if index < len(variants) and variants[index] else "")
for index, layout in enumerate(layouts)]
def picker_rows(catalog, selected):
# Current entries stay first, in switching order, followed by the catalog.
keys = list(dict.fromkeys(selected + sorted(catalog, key=lambda key: catalog[key])))
labels = {key: catalog.get(key, key) for key in keys}
counts = Counter(labels.values())
rows = {key: "\t" + labels[key] + (f" ({key})" if counts[labels[key]] > 1 else "") for key in keys}
return rows
def choose(title, catalog, selected, kind, overrides=None):
catalog = {**catalog, **{key: catalog.get(key, key) for key in selected}}
rows = picker_rows(catalog, selected)
args = ["omarchy-menu-select", title] + list(rows.values())
on_change = ["/usr/bin/python3", str(setup.ROOT / "default/input-methods/typing_setup.py"), "apply", kind, json.dumps({"catalog": catalog, "overrides": overrides or {}})]
args += ["--", "--multiple", "--width", "620", "--maxheight", "650", "--on-change", json.dumps(on_change), "--change-key", "typing:" + kind]
for key in selected:
args += ["--selected", rows[key][1:]]
result = subprocess.run(args, text=True, capture_output=True)
if result.returncode == 1:
return None
if result.returncode:
raise RuntimeError(result.stderr.strip() or "The selection menu could not open")
def input_catalog(items):
available = {item[0]: item[1] for item in setup.available_methods()}
catalog = {key: preset["label"] for key, preset in setup.PRESETS.items() if key != "none"}
for name, layout in items:
if not name.startswith("keyboard-"):
catalog.setdefault(name, available.get(name, name))
return catalog, available
def input_items(items, selected, overrides=None):
if not any(item[0].startswith("keyboard-") for item in items):
raise RuntimeError("The current input group has no keyboard input")
# Retained entries keep their switching order; new engines follow them.
kept = [item for item in items if item[0].startswith("keyboard-") or item[0] in selected]
current = dict(items)
return kept + [[name, (overrides or {}).get(name, "")] for name in selected if name not in current]
def set_inputs(group, layout, before, after):
if before == after:
return
try:
# Removing the active engine must return typing to the keyboard.
setup.run(["fcitx5-remote", "-c"])
setup.live_set(group, layout, after)
if setup.live_group() != (group, layout, after):
raise RuntimeError("Fcitx did not retain the input selection")
except BaseException:
setup.live_set(group, layout, before)
raise
def configure_inputs():
group, layout, items = setup.live_group()
catalog, available = input_catalog(items)
current = [name for name, override in items if not name.startswith("keyboard-")]
choose("Input Methods", catalog, current, "input", dict(items))
def load_engines(names, catalog):
packages = sorted({package for name in names for package in setup.PRESETS.get(name, {}).get("packages", [])})
absent = [package for package in packages
if subprocess.run(["omarchy-pkg-present", package], capture_output=True).returncode]
if absent or any(name not in setup.PRESETS for name in names):
labels = ", ".join(catalog.get(name, name) for name in names)
raise RuntimeError(f"Install {' '.join(absent) or 'its engine'} to use {labels}")
# Fcitx discovers engines only at startup, so one installed while it was
# running (by an update, say) needs a restart before it can be selected.
setup.run(["omarchy-restart-xcompose"])
for name in names:
setup.wait_ready(name)
def save_inputs(selected, overrides=None):
group, layout, items = setup.live_group()
catalog, available = input_catalog(items)
missing = [name for name in selected if name not in available]
if missing:
load_engines(missing, catalog)
group, layout, items = setup.live_group()
set_inputs(group, layout, items, input_items(items, selected, overrides))
# Only a saved engine claims the CJK font fallback, and a failed font write
# returns the group so the menu's restored checks stay true.
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config"))
try:
for name in selected:
if name in setup.PRESETS:
setup.font_default(config_home, name)
except BaseException:
setup.live_set(group, layout, items)
raise
def add_input(method):
group, layout, items = setup.live_group()
current = [name for name, override in items if not name.startswith("keyboard-")]
if method not in current:
save_inputs(current + [method])
def keyboard_values(selected):
if not selected:
raise ValueError("Select at least one keyboard layout")
parts = [value.split(":", 1) for value in selected]
# Preserve the installer's Latin-leading rule for desktop keybindings.
if parts[0][0] in setup.NON_LATIN:
parts = [["us"]] + [part for part in parts if part != ["us"]]
layouts = ",".join(part[0] for part in parts)
variants = ",".join(part[1] if len(part) > 1 else "" for part in parts)
return layouts, variants
def save_keyboard(selected):
layouts, variants = keyboard_values(selected)
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config"))
path = config_home / "omarchy/keyboard-layouts"
original = setup.read(path) if path.exists() else None
group, old_layout, before = setup.live_group()
group_changed = False
previous_errors = setup.run(["hyprctl", "configerrors"])
try:
setup.atomic_write(path, f"XKBLAYOUT={layouts}\nXKBVARIANT={variants}\n")
setup.run(["hyprctl", "reload"])
errors = setup.run(["hyprctl", "configerrors"])
if errors and errors != previous_errors:
raise RuntimeError(errors)
expected = [layout + (":" + variant if variant else "")
for layout, variant in zip(layouts.split(","), variants.split(","))]
if keyboard_selection() != expected:
raise RuntimeError("Your personal keyboard layout override takes precedence over this selection")
first_layout = layouts.split(",")[0]
first_variant = variants.split(",")[0]
group_layout = first_layout + ("-" + first_variant if first_variant else "")
primary = next((item[0] for item in before if item[0].startswith("keyboard-")), None)
if primary is None:
raise RuntimeError("The current input group has no keyboard input")
keyboard = "keyboard-" + group_layout
after = [[keyboard, ""]] + [item for item in before if item[0] not in (primary, keyboard)]
group_changed = True
setup.live_set(group, group_layout, after)
if setup.live_group() != (group, group_layout, after):
raise RuntimeError("Fcitx did not retain the keyboard selection")
except BaseException:
if original is None:
path.resolve().unlink(missing_ok=True)
else:
setup.atomic_write(path, original)
try:
setup.run(["hyprctl", "reload"])
finally:
if group_changed:
setup.live_set(group, old_layout, before)
raise
def configure_keyboard():
current = keyboard_selection()
catalog = keyboard_catalog()
choose("Keyboard Layouts", catalog, current, "keyboard")
def apply_selection(kind, context, values):
catalog = context["catalog"]
rows = picker_rows(catalog, [])
valid = {row[1:]: key for key, row in rows.items()}
if not isinstance(values, list) or any(not isinstance(value, str) or value not in valid for value in values):
raise ValueError("The menu returned an invalid selection")
selected = list(dict.fromkeys(valid[value] for value in values))
if kind == "input":
save_inputs(selected, context.get("overrides"))
else:
save_keyboard(selected)
selected = keyboard_selection()
print(json.dumps([rows[key][1:] for key in selected]))
def main():
if sys.argv[1:2] == ["add"] and len(sys.argv) == 3:
try:
add_input(sys.argv[2])
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
raise SystemExit(f"Input method could not be added: {error}")
return
applying = len(sys.argv) == 5 and sys.argv[1] == "apply"
kind = sys.argv[2] if applying else sys.argv[1] if len(sys.argv) == 2 else ""
if kind not in ("input", "keyboard"):
raise ValueError("Choose keyboard or input setup")
title = "Input methods" if kind == "input" else "Keyboard layouts"
try:
if applying:
apply_selection(kind, json.loads(sys.argv[3]), json.loads(sys.argv[4]))
elif kind == "input":
configure_inputs()
else:
configure_keyboard()
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
subprocess.run(["omarchy-notification-send", title + " could not be updated", str(error)], check=True)
raise SystemExit(1)
if __name__ == "__main__":
main()
+56 -20
View File
@@ -53,7 +53,60 @@
"learn.community": {"icon":"󰙯","label":"Community","action":"omarchy-launch-discord-community"}, "learn.community": {"icon":"󰙯","label":"Community","action":"omarchy-launch-discord-community"},
// Trigger // Trigger
"trigger.appearance": {"icon":"󰏘","label":"Appearance"},
"trigger.appearance.animations": {"icon":"󰗘","label":"Animations","action":"omarchy-toggle-animations"},
"trigger.appearance.top-bar": {"icon":"󰍜","label":"Menu Bar"},
"trigger.appearance.top-bar.hide": {"icon":"󰈉","label":"Hide Bar","when":"! omarchy-toggle-enabled bar-off","action":"omarchy-toggle-bar"},
"trigger.appearance.top-bar.show": {"icon":"󰈈","label":"Show Bar","when":"omarchy-toggle-enabled bar-off","action":"omarchy-toggle-bar"},
"trigger.appearance.top-bar.position": {"icon":"","label":"Position Bar"},
"trigger.appearance.top-bar.background": {"icon":"󰂵","label":"Bar Background"},
"trigger.appearance.top-bar.background.opaque": {"icon":"󰝤","label":"Opaque Bar","checked":"[[ $(omarchy-bar background) == \"opaque\" ]]","action":"omarchy-bar background opaque"},
"trigger.appearance.top-bar.background.glassy": {"icon":"󰂵","label":"Glassy Bar","checked":"[[ $(omarchy-bar background) == \"glassy\" ]]","action":"omarchy-bar background glassy"},
"trigger.appearance.top-bar.background.clear": {"icon":"󰝣","label":"Clear Bar","checked":"[[ $(omarchy-bar background) == \"none\" ]]","action":"omarchy-bar background none"},
"trigger.appearance.top-bar.indicators": {"icon":"󰋽","label":"Bar Indicators"},
"trigger.appearance.top-bar.indicators.battery-percentage": {"icon":"󰁹","label":"Battery Percentage","when":"omarchy-hw-laptop","action":"omarchy-shell omarchy.power togglePercentage"},
"trigger.appearance.top-bar.indicators.clock": {"icon":"󰥔","label":"Clock"},
"trigger.appearance.top-bar.indicators.clock.sunday-24h": {"icon":"󰥔","label":"Sunday 14:30","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"dddd HH:mm\" ]]","action":"omarchy-bar set omarchy.clock format \"dddd HH:mm\""},
"trigger.appearance.top-bar.indicators.clock.sunday-12h": {"icon":"󰥔","label":"Sunday 2:30 PM","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"dddd h:mm AP\" ]]","action":"omarchy-bar set omarchy.clock format \"dddd h:mm AP\""},
"trigger.appearance.top-bar.indicators.clock.sunday-seconds-24h": {"icon":"󰥔","label":"Sunday 14:30:05","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"dddd HH:mm:ss\" ]]","action":"omarchy-bar set omarchy.clock format \"dddd HH:mm:ss\""},
"trigger.appearance.top-bar.indicators.clock.sunday-seconds-12h": {"icon":"󰥔","label":"Sunday 2:30:05 PM","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"dddd h:mm:ss AP\" ]]","action":"omarchy-bar set omarchy.clock format \"dddd h:mm:ss AP\""},
"trigger.appearance.top-bar.indicators.clock.time-24h": {"icon":"󰥔","label":"14:30","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"HH:mm\" ]]","action":"omarchy-bar set omarchy.clock format \"HH:mm\""},
"trigger.appearance.top-bar.indicators.clock.time-12h": {"icon":"󰥔","label":"2:30 PM","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"h:mm AP\" ]]","action":"omarchy-bar set omarchy.clock format \"h:mm AP\""},
"trigger.appearance.top-bar.indicators.clock.date-24h": {"icon":"󰥔","label":"Sun 3 May 14:30","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"ddd d MMM HH:mm\" ]]","action":"omarchy-bar set omarchy.clock format \"ddd d MMM HH:mm\""},
"trigger.appearance.top-bar.indicators.clock.date-12h": {"icon":"󰥔","label":"Sun 3 May 2:30 PM","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"ddd d MMM h:mm AP\" ]]","action":"omarchy-bar set omarchy.clock format \"ddd d MMM h:mm AP\""},
"trigger.appearance.top-bar.indicators.clock.week": {"icon":"󰥔","label":"3 May W18 2026","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"d MMMM 'W'ww yyyy\" ]]","action":"omarchy-bar set omarchy.clock format \"d MMMM 'W'ww yyyy\""},
"trigger.appearance.top-bar.indicators.clock.iso": {"icon":"󰥔","label":"2026-05-03 14:30","when":"[[ $(omarchy-bar position) != \"left\" && $(omarchy-bar position) != \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock format) == \"yyyy-MM-dd HH:mm\" ]]","action":"omarchy-bar set omarchy.clock format \"yyyy-MM-dd HH:mm\""},
"trigger.appearance.top-bar.indicators.clock.stacked-24h": {"icon":"󰥔","label":"14 — 30","when":"[[ $(omarchy-bar position) == \"left\" || $(omarchy-bar position) == \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock verticalFormat) == $'HH\\n—\\nmm' ]]","action":"omarchy-bar set omarchy.clock verticalFormat $'HH\\n—\\nmm'"},
"trigger.appearance.top-bar.indicators.clock.stacked-12h": {"icon":"󰥔","label":"2 — 30 PM","when":"[[ $(omarchy-bar position) == \"left\" || $(omarchy-bar position) == \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock verticalFormat) == $'h\\n—\\nmm\\nAP' ]]","action":"omarchy-bar set omarchy.clock verticalFormat $'h\\n—\\nmm\\nAP'"},
"trigger.appearance.top-bar.indicators.clock.stacked-date": {"icon":"󰥔","label":"03 May W18 '26","when":"[[ $(omarchy-bar position) == \"left\" || $(omarchy-bar position) == \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock verticalFormat) == $'dd\\nMMM\\n\\'W\\'ww\\n\\'\\'yy' ]]","action":"omarchy-bar set omarchy.clock verticalFormat $'dd\\nMMM\\n\\'W\\'ww\\n\\'\\'yy'"},
"trigger.appearance.top-bar.indicators.clock.stacked-time": {"icon":"󰥔","label":"14 30","when":"[[ $(omarchy-bar position) == \"left\" || $(omarchy-bar position) == \"right\" ]]","checked":"[[ $(omarchy-bar get omarchy.clock verticalFormat) == $'HH\\nmm' ]]","action":"omarchy-bar set omarchy.clock verticalFormat $'HH\\nmm'"},
"trigger.appearance.top-bar.position.top": {"icon":"󰁝","label":"Top Bar","action":"omarchy-bar position top"},
"trigger.appearance.top-bar.position.bottom": {"icon":"󰁅","label":"Bottom Bar","action":"omarchy-bar position bottom"},
"trigger.appearance.top-bar.position.left": {"icon":"󰁍","label":"Left Bar","action":"omarchy-bar position left"},
"trigger.appearance.top-bar.position.right": {"icon":"󰁔","label":"Right Bar","action":"omarchy-bar position right"},
"trigger.appearance.window-gaps": {"icon":"","label":"Window Gaps","action":"omarchy-hyprland-window-gaps-toggle"},
"trigger.appearance.window-corners": {"icon":"󰘇","label":"Window Corners"},
"trigger.appearance.window-corners.theme": {"icon":"󰏘","label":"Theme Corners","checked":"[[ $(omarchy-hyprland-corners) == \"theme\" ]]","action":"omarchy-hyprland-corners theme"},
"trigger.appearance.window-corners.sharp": {"icon":"󰝣","label":"Sharp Corners","checked":"[[ $(omarchy-hyprland-corners) == \"sharp\" ]]","action":"omarchy-hyprland-corners sharp"},
"trigger.appearance.window-corners.rounded": {"icon":"󰘇","label":"Rounded Corners","checked":"[[ $(omarchy-hyprland-corners) == \"rounded\" ]]","action":"omarchy-hyprland-corners rounded"},
"trigger.appearance.window-transparency": {"icon":"󰂵","label":"Window Transparency"},
"trigger.appearance.window-transparency.opaque": {"icon":"󰝤","label":"Opaque Windows","checked":"[[ $(omarchy-hyprland-window-transparency) == \"opaque\" ]]","action":"omarchy-hyprland-window-transparency opaque"},
"trigger.appearance.window-transparency.transparent": {"icon":"󰝣","label":"Transparent Windows","checked":"[[ $(omarchy-hyprland-window-transparency) == \"transparent\" ]]","action":"omarchy-hyprland-window-transparency transparent"},
"trigger.appearance.window-transparency.glassy": {"icon":"󰂵","label":"Glassy Windows","checked":"[[ $(omarchy-hyprland-window-transparency) == \"glassy\" ]]","action":"omarchy-hyprland-window-transparency glassy"},
"trigger.appearance.one-window-ratio": {"icon":"","label":"1-Window Ratio","action":"omarchy-hyprland-window-single-square-aspect-toggle"},
"trigger.appearance.workspace-layout": {"icon":"󱂬","label":"Workspace Layout","title":"Default Workspace Layout"},
"trigger.appearance.workspace-layout.dwindle": {"icon":"󱓻","label":"Tiling","checked":"[[ $(omarchy-hyprland-workspace-layout-default) == \"dwindle\" ]]","action":"omarchy-hyprland-workspace-layout-default dwindle"},
"trigger.appearance.workspace-layout.scrolling": {"icon":"󰝥","label":"Scrolling","checked":"[[ $(omarchy-hyprland-workspace-layout-default) == \"scrolling\" ]]","action":"omarchy-hyprland-workspace-layout-default scrolling"},
"trigger.appearance.workspace-layout.floating": {"icon":"󰔶","label":"Floating","checked":"[[ $(omarchy-hyprland-workspace-layout-default) == \"floating\" ]]","action":"omarchy-hyprland-workspace-layout-default floating"},
"trigger.appearance.theme-sync": {"icon":"󰸌","label":"Herdr Theme Sync","when":"omarchy-theme-set-herdr-machines --available","action":"omarchy-toggle-theme-sync"},
"trigger.toggle": {"icon":"󰔎","label":"Toggle"},
"trigger.toggle.idle-lock": {"icon":"󰅶","label":"Stay Awake","action":"omarchy-toggle-idle"},
"trigger.toggle.notifications": {"icon":"󰂛","label":"Notifications","action":"omarchy-toggle-notification-silencing"},
"trigger.toggle.nightlight": {"icon":"󰔎","label":"Nightlight","action":"omarchy-toggle-nightlight"},
"trigger.toggle.screensaver": {"icon":"󱄄","label":"Screensaver","action":"omarchy-toggle-screensaver"},
"trigger.toggle.crash-capture": {"icon":"󱚡","label":"Crash Capture","action":"omarchy-toggle-crash-capture"},
"trigger.emoji": {"icon":"","label":"Emoji","aliases":["emoji","emojis"],"action":"omarchy-menu-emoji"}, "trigger.emoji": {"icon":"","label":"Emoji","aliases":["emoji","emojis"],"action":"omarchy-menu-emoji"},
"trigger.clipboard-history": {"icon":"󰅇","label":"Clipboard History","action":"omarchy-shell shell summon omarchy.clipboard"},
"trigger.reminder": {"icon":"󰢌","label":"Reminder","aliases":["reminder"]}, "trigger.reminder": {"icon":"󰢌","label":"Reminder","aliases":["reminder"]},
"trigger.capture": {"icon":"","label":"Capture","aliases":["capture","screenshot","screenrecord","screen-record","screenrecording"]}, "trigger.capture": {"icon":"","label":"Capture","aliases":["capture","screenshot","screenrecord","screen-record","screenrecording"]},
"trigger.capture.screenshot": {"icon":"","label":"Screenshot","action":"omarchy-capture-screenshot"}, "trigger.capture.screenshot": {"icon":"","label":"Screenshot","action":"omarchy-capture-screenshot"},
@@ -68,7 +121,6 @@
"trigger.capture.screenrecord.webcam": {"icon":"","label":"With desktop + microphone audio + webcam","when":"omarchy-hw-webcam","action":"omarchy-capture-screenrecording-with-webcam"}, "trigger.capture.screenrecord.webcam": {"icon":"","label":"With desktop + microphone audio + webcam","when":"omarchy-hw-webcam","action":"omarchy-capture-screenrecording-with-webcam"},
"trigger.transcode": {"icon":"󰧸","label":"Transcode","action":"omarchy-transcode"}, "trigger.transcode": {"icon":"󰧸","label":"Transcode","action":"omarchy-transcode"},
"trigger.share": {"icon":"","label":"Share","aliases":["share"]}, "trigger.share": {"icon":"","label":"Share","aliases":["share"]},
"trigger.toggle": {"icon":"󰔎","label":"Toggle","aliases":["toggle","toggles"]},
"trigger.hardware": {"icon":"","label":"Hardware","aliases":["hardware","hw"]}, "trigger.hardware": {"icon":"","label":"Hardware","aliases":["hardware","hw"]},
"trigger.tests": {"icon":"󰓅","label":"Speed Test"}, "trigger.tests": {"icon":"󰓅","label":"Speed Test"},
"trigger.hardware.laptop-display": {"icon":"󰛧","label":"Laptop Display","when":"omarchy-hw-laptop","action":"omarchy-hyprland-monitor-internal toggle"}, "trigger.hardware.laptop-display": {"icon":"󰛧","label":"Laptop Display","when":"omarchy-hw-laptop","action":"omarchy-hyprland-monitor-internal toggle"},
@@ -87,18 +139,6 @@
"trigger.share.file": {"icon":"","label":"File","action":"omarchy-menu-share file"}, "trigger.share.file": {"icon":"","label":"File","action":"omarchy-menu-share file"},
"trigger.share.folder": {"icon":"","label":"Folder","action":"omarchy-menu-share folder"}, "trigger.share.folder": {"icon":"","label":"Folder","action":"omarchy-menu-share folder"},
"trigger.share.receive": {"icon":"󰥦","label":"Receive","action":"uwsm-app -- localsend"}, "trigger.share.receive": {"icon":"󰥦","label":"Receive","action":"uwsm-app -- localsend"},
"trigger.toggle.idle-lock": {"icon":"󰅶","label":"Stay Awake","action":"omarchy-toggle-idle"},
"trigger.toggle.notifications": {"icon":"󰂛","label":"Notifications","action":"omarchy-toggle-notification-silencing"},
"trigger.toggle.crash-capture": {"icon":"󱚡","label":"Crash Capture","action":"omarchy-toggle-crash-capture"},
"trigger.toggle.screensaver": {"icon":"󱄄","label":"Screensaver","action":"omarchy-toggle-screensaver"},
"trigger.toggle.theme-sync": {"icon":"󰸌","label":"Herdr Theme Sync","when":"omarchy-theme-set-herdr-machines --available","action":"omarchy-toggle-theme-sync"},
"trigger.toggle.nightlight": {"icon":"󰔎","label":"Nightlight","action":"omarchy-toggle-nightlight"},
"trigger.toggle.top-bar": {"icon":"󰍜","label":"Menu Bar","action":"omarchy-toggle-bar"},
"trigger.toggle.battery-percentage": {"icon":"󰁹","label":"Battery Percentage","when":"omarchy-hw-laptop","action":"omarchy-shell omarchy.power togglePercentage"},
"trigger.toggle.workspace-layout": {"icon":"󱂬","label":"Workspace Layout","action":"omarchy-hyprland-workspace-layout-toggle"},
"trigger.toggle.window-gaps": {"icon":"","label":"Window Gaps","action":"omarchy-hyprland-window-gaps-toggle"},
"trigger.toggle.animations": {"icon":"󰕟","label":"Animations","action":"omarchy-toggle-animations"},
"trigger.toggle.one-window-ratio": {"icon":"","label":"1-Window Ratio","action":"omarchy-hyprland-window-single-square-aspect-toggle"},
"trigger.tests.network-speedtest": {"icon":"󰓅","label":"Network Speed Test","action":"omarchy-shell shell summon omarchy.speedtest"}, "trigger.tests.network-speedtest": {"icon":"󰓅","label":"Network Speed Test","action":"omarchy-shell shell summon omarchy.speedtest"},
"trigger.tests.disk-speedtest": {"icon":"󰋊","label":"Disk Speed Test","action":"omarchy-shell shell summon omarchy.disk-speedtest"}, "trigger.tests.disk-speedtest": {"icon":"󰋊","label":"Disk Speed Test","action":"omarchy-shell shell summon omarchy.disk-speedtest"},
@@ -107,16 +147,9 @@
"style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""}, "style.background": {"icon":"","label":"Background","aliases":["background","wallpaper"],"action":"background=$(omarchy-theme-bg-switcher); [[ -n $background ]] && omarchy-theme-bg-set \"$background\""},
"style.unlock": {"icon":"󰟵","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme $(printf %q \"$unlock\")\"; fi"}, "style.unlock": {"icon":"󰟵","label":"Unlock","aliases":["unlock"],"action":"unlock=$(omarchy-plymouth-switcher); if [[ $unlock == default ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-plymouth-reset; elif [[ -n $unlock ]]; then omarchy-launch-floating-terminal-with-presentation \"omarchy-plymouth-set-by-theme $(printf %q \"$unlock\")\"; fi"},
"style.font": {"icon":"","label":"Font","provider":"fonts"}, "style.font": {"icon":"","label":"Font","provider":"fonts"},
"style.bar": {"icon":"󰍜","label":"Menu Bar"},
"style.bar.position": {"icon":"","label":"Position"},
"style.bar.transparency": {"icon":"󰂵","label":"Transparency","action":"omarchy-bar transparent toggle"},
"style.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/looknfeel.lua\""}, "style.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/looknfeel.lua\""},
"style.screensaver": {"icon":"󱄄","label":"Screensaver"}, "style.screensaver": {"icon":"󱄄","label":"Screensaver"},
"style.about": {"icon":"","label":"About"}, "style.about": {"icon":"","label":"About"},
"style.bar.position.top": {"icon":"󰁝","label":"Top","action":"omarchy-bar position top"},
"style.bar.position.bottom": {"icon":"󰁅","label":"Bottom","action":"omarchy-bar position bottom"},
"style.bar.position.left": {"icon":"󰁍","label":"Left","action":"omarchy-bar position left"},
"style.bar.position.right": {"icon":"󰁔","label":"Right","action":"omarchy-bar position right"},
"style.about.text": {"icon":"","label":"Edit Text","action":"omarchy-branding-about text"}, "style.about.text": {"icon":"","label":"Edit Text","action":"omarchy-branding-about text"},
"style.about.image": {"icon":"","label":"Set From Image","action":"omarchy-branding-about image"}, "style.about.image": {"icon":"","label":"Set From Image","action":"omarchy-branding-about image"},
"style.about.default": {"icon":"","label":"Restore Default","action":"omarchy-branding-about reset"}, "style.about.default": {"icon":"","label":"Restore Default","action":"omarchy-branding-about reset"},
@@ -128,6 +161,9 @@
"setup.monitors": {"icon":"󰍹","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""}, "setup.monitors": {"icon":"󰍹","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""},
"setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""}, "setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""},
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""}, "setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
"setup.typing": {"icon":"","label":"Typing"},
"setup.typing.keyboard": {"icon":"","label":"Keyboard Layouts","action":"omarchy-setup-keyboard"},
"setup.typing.input": {"icon":"","label":"Input Methods","action":"omarchy-setup-input"},
"setup.network": {"icon":"󰛳","label":"Network","aliases":["network"]}, "setup.network": {"icon":"󰛳","label":"Network","aliases":["network"]},
"setup.network.dns": {"icon":"󰇖","label":"DNS","aliases":["dns"]}, "setup.network.dns": {"icon":"󰇖","label":"DNS","aliases":["dns"]},
"setup.network.dns.dhcp": {"icon":"󰩟","label":"DHCP","checked":"[[ \"$(omarchy-dns)\" == \"DHCP\" ]]","action":"omarchy-dns DHCP"}, "setup.network.dns.dhcp": {"icon":"󰩟","label":"DHCP","checked":"[[ \"$(omarchy-dns)\" == \"DHCP\" ]]","action":"omarchy-dns DHCP"},
+4 -3
View File
@@ -1,6 +1,6 @@
[Unit] [Unit]
Description=Fcitx5 input method (XCompose sequences) Description=Fcitx5 input methods and XCompose sequences
# fcitx5 turns the CapsLock compose sequences in ~/.XCompose into text for # fcitx5 turns the Shift + CapsLock compose sequences in ~/.XCompose into text for
# Wayland clients. # Wayland clients.
# #
# Wait for the compositor: fcitx5 needs WAYLAND_DISPLAY and DISPLAY, which uwsm # Wait for the compositor: fcitx5 needs WAYLAND_DISPLAY and DISPLAY, which uwsm
@@ -19,7 +19,8 @@ PartOf=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY ConditionEnvironment=WAYLAND_DISPLAY
[Service] [Service]
Type=simple Type=dbus
BusName=org.fcitx.Fcitx5
# notificationitem duplicates the tray entry omarchy already renders itself. # notificationitem duplicates the tray entry omarchy already renders itself.
ExecStart=/usr/bin/fcitx5 --disable notificationitem ExecStart=/usr/bin/fcitx5 --disable notificationitem
# always, not on-failure: fcitx5 exits 0 when it detects another instance owning # always, not on-failure: fcitx5 exits 0 when it detects another instance owning
+3
View File
@@ -1,6 +1,9 @@
[colors-dark] [colors-dark]
foreground={{ foreground_strip }} foreground={{ foreground_strip }}
background={{ background_strip }} background={{ background_strip }}
# Blur only applies while the background is see-through, as with Glassy windows,
# and foot reads it only at launch, so it is on from the start.
blur=yes
selection-foreground={{ selection_foreground_strip }} selection-foreground={{ selection_foreground_strip }}
selection-background={{ selection_background_strip }} selection-background={{ selection_background_strip }}
+4
View File
@@ -6,6 +6,10 @@ selection_background {{ selection_background }}
cursor {{ bright_foreground }} cursor {{ bright_foreground }}
cursor_text_color {{ background }} cursor_text_color {{ background }}
# Lets a reload change the background opacity, as Glassy windows do, since
# kitty only allows that when this is on from launch.
dynamic_background_opacity yes
active_border_color {{ accent }} active_border_color {{ accent }}
active_tab_background {{ accent }} active_tab_background {{ accent }}
+2
View File
@@ -91,6 +91,8 @@ config/** ──► omarchy-settings /etc/skel/.config/
/usr/share/omarchy/config/** (resync source) /usr/share/omarchy/config/** (resync source)
etc/fastfetch/config.jsonc ──► omarchy-settings /etc/fastfetch/config.jsonc etc/fastfetch/config.jsonc ──► omarchy-settings /etc/fastfetch/config.jsonc
etc/xdg/kitty/kitty.conf ──► omarchy-settings /etc/xdg/kitty/kitty.conf etc/xdg/kitty/kitty.conf ──► omarchy-settings /etc/xdg/kitty/kitty.conf
etc/xkb/{rules/evdev,symbols/omarchy}
──► omarchy-settings /etc/xkb/ (Shift + Caps Lock compose option)
applications/*.desktop ──► omarchy-settings /etc/skel/.local/share/applications/ applications/*.desktop ──► omarchy-settings /etc/skel/.local/share/applications/
/usr/share/omarchy/applications/ /usr/share/omarchy/applications/
+25
View File
@@ -0,0 +1,25 @@
# Input methods
The install keyboard picker selects a console keymap plus an optional Fcitx engine. `install/provisioning/setup-form.sh` is shared by the ISO configurator and deferred first-boot owner setup. Its rows are `label|console keymap|input method|desktop XKB override`; the last two fields are optional, so ordinary keyboard choices keep their existing meaning.
The ISO carries the extra fields in `omarchy_install.input_method` and `omarchy_install.input_xkb_layout`. Both installation paths persist them in `/etc/omarchy/input-method` as `INPUT_METHOD` and `XKB_LAYOUT`. This file is parsed as data, never sourced. Korean and Lao have a real US console keymap and a `kr` or `la` desktop override (Lao is non-Latin, so US still leads on the desktop); Japanese uses `jp106`, or US when the US-keyboard choice is selected. Console and disk-unlock input remain ordinary keymaps.
`install/user/input-method.sh` calls `omarchy-input-method configure --seed` during user finalization, before graphical login. The standard engines are in the base package set so deferred provisioning and factory resets do not need network access to configure typing. `default/input-methods/presets.json` is the engine catalog used by the post-install selector.
`default/input-methods/configure.py` owns the shared configuration behavior. Offline seeding replaces only a missing profile or Fcitx's untouched single `keyboard-us` guess. The group layout matches Hyprland's effective leading layout and variant; non-Latin layouts retain the US-leading behavior used for desktop bindings. The selected composition engine is the group's `DefaultIM`, with `ActiveByDefault=False` so initial typing remains Latin. Existing multilingual profiles and explicitly chosen keyboard layouts are preserved.
`omarchy setup input <engine>` installs the selected engine's packages, then appends it through the same path as the Input Methods picker. Selecting an engine whose package is installed but which the running Fcitx has not discovered (one an update installed, say) restarts the supervised Fcitx unit once and waits for it; an engine whose package is missing is refused with the package name. Changes go through `InputMethodGroupInfo`, `SetInputMethodGroupInfo`, and `Save`, keeping the group name, layout, existing methods in their switching order, and their layout overrides. There is no parallel unsupervised daemon. A per-user D-Bus activation file delegates `org.fcitx.Fcitx5` to `omarchy-fcitx5.service`.
`Setup > Typing > Keyboard Layouts` and `Input Methods` use the native menu's multi-select mode, with current entries checked and changes applied immediately. `default/input-methods/typing_setup.py` uses the installer’s keyboard choices, converted to desktop layouts through systemd’s keymap table, and reads the current input group. Input selection replaces only the non-keyboard group items and retains selected engines' layout overrides; deselection does not uninstall packages. The standard composition engines are already shipped in the base package set. Closing a picker keeps changes already applied. Failed updates leave its checked state unchanged.
Keyboard selections are persisted as data in `~/.config/omarchy/keyboard-layouts`; `default.hypr.keyboard.selected()` reads them before falling back to installation defaults. Personal Hyprland overrides still take precedence. A save reloads and validates the compositor, aligns the Fcitx group's primary keyboard with the leading layout/variant, and restores the previous configuration and group on failure. Non-Latin selections keep the installer's Latin-leading rule for desktop shortcuts. System console, disk-unlock, and greeter layouts are unchanged.
Shortcut defaults reserve Ctrl+Space for Tmux/Herdr, Super+Space for the menu, Super+Shift+Space for the bar, and Super+grave for the console. Hyprland owns Super+I to cycle all selected compositor layouts followed by the configured Fcitx input methods, independently of Fcitx switching keys. With keyboard layouts alone, the same shortcut cycles the compositor layouts, keeping matching devices in sync even without the bar widget. The widget is enabled in the default right-hand bar layout and appears only when more than one input method or keyboard layout is configured. Fcitx handles Zenkaku/Hankaku and Hangul; Henkan activates and Muhenkan deactivates. Ctrl+Space is never a Fcitx toggle: `configure --defaults` removes it from any TriggerKeys list, keeping the user's other keys, and writes Omarchy's list where none exists, since Fcitx's built-in default includes it. Other existing lists, including empty ones, are preserved, and stock generated lists are repaired only while the user still has the single-keyboard default profile. A terminal Ctrl+Space returns to direct input before the following key, with a 300 ms timeout and no D-Bus activation. Wayland and XCB addons do not override the compositor's XKB settings.
The migration installs the standard engines and applies conservative defaults. It aligns an untouched Fcitx keyboard profile with the install layout, using the live controller when Fcitx is running. It leaves existing multilingual profiles and working trigger keys alone. Atomic file writes follow configuration symlinks, preserve file modes, and roll back on a failed seed or migration so a retry can complete. Migration bus errors defer profile alignment to the next login and do not restart input. If a pre-update daemon has not discovered a new engine, the migration aligns only its keyboard and prints the setup command.
Input selection does not change the system locale, repository region, desktop language, or Latin interface font. A new language setup seeds a CJK fallback preference once; additional languages do not overwrite it.
Focused checks live in `test/shell.d/input-method-test.sh`, `setup-form-test.sh`, `hyprland-keyboard-layout-test.sh`, and `hyprland-jis-bindings-test.sh`. `test/acceptance.d/input-methods-test.sh` exercises real composition in a disposable installed VM and restores the original user configuration. The ISO repository's keyboard tests cover transport and target persistence. Browser input must be verified against the shipped browser versions; the language selector is not a place to guess flags from older guides.
Default key values are checked against [Fcitx global configuration](https://github.com/fcitx/fcitx5/blob/master/src/lib/fcitx/globalconfig.cpp).
+7
View File
@@ -165,3 +165,10 @@ renders under the label, filters with it, and comes back as
`label\tsubtext` so callers with same-named rows get a stable key. This is `label\tsubtext` so callers with same-named rows get a stable key. This is
how the pickers behind menu actions (`omarchy-menu-plugin`, how the pickers behind menu actions (`omarchy-menu-plugin`,
`omarchy-menu-timezone`, ...) present lists without owning any UI. `omarchy-menu-timezone`, ...) present lists without owning any UI.
Pass `-- --multiple` to `omarchy-menu-select` for a multi-select request. Repeat `--selected <value>` to preselect returned values (labels plus any subtext, without icons). Clicking or pressing Enter toggles an option without closing the menu; Apply returns a JSON array in option order, including `[]` when all choices are deselected. Cancellation still exits 1. Selections survive filtering.
With `--on-change <JSON command array>`, each toggle runs that command with the proposed selection JSON appended as its final argument, and no Apply row is shown. The checkbox changes only after the command succeeds; a failed command retains the previous selection. Changes persist when the menu closes. The typing selectors use this mode.
For immediate multi-select requests, `--change-key <key>` identifies the setting being changed across picker openings. The menu keeps queued selections for that key when reopened, so a new toggle cannot discard a change still being saved. Without a key, the command array identifies the setting.
+5 -4
View File
@@ -94,8 +94,8 @@ You can still install by hand: drop a plugin into
`~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then `~/.config/omarchy/plugins/<id>/`, run `omarchy-shell shell rescanPlugins`, then
`omarchy plugin enable <id>`. A bar widget starts in its declared default `omarchy plugin enable <id>`. A bar widget starts in its declared default
section; enabling a full bar replaces the one in use. `omarchy bar` drives the section; enabling a full bar replaces the one in use. `omarchy bar` drives the
bar from the CLI — `use | reset | defaults | position | transparent | put | bar from the CLI — `use | reset | defaults | position | background | put |
move | set`, with placement flags such as `--section` and `--index`. move | set | get`, with placement flags such as `--section` and `--index`.
The lower-level IPC methods remain available through `omarchy-shell shell ...`. The lower-level IPC methods remain available through `omarchy-shell shell ...`.
## Elsewhen ## Elsewhen
@@ -121,7 +121,8 @@ or `omarchy.power`. There is no `bar` target.
| `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code | | `rescanPlugins` | re-walk plugin dirs and hot-reload plugin code |
| `reloadConfig` | reload shell.json | | `reloadConfig` | reload shell.json |
| `applyTheme <colorsB64> <shellB64>` | push theme colors + shell.toml | | `applyTheme <colorsB64> <shellB64>` | push theme colors + shell.toml |
| `toggleBarTransparency` | flip the bar background between solid and transparent | | `toggleBarTransparency` | cycle the bar background from clear to glassy to opaque |
| `cycleInput <next\|back>` | switch to the next or previous input method or layout (Super+I, Super+Shift+I); `not running` without the input reader |
| `setPluginEnabled <id> <"true"\|…>` | flip enabled bit (`ok` / `unknown`) | | `setPluginEnabled <id> <"true"\|…>` | flip enabled bit (`ok` / `unknown`) |
| `enablePlugin <id> <placementJson>` | enable and place in one mutation | | `enablePlugin <id> <placementJson>` | enable and place in one mutation |
| `putBarWidget <id> <placementJson>` | place a widget only where absent (`omarchy bar put`) | | `putBarWidget <id> <placementJson>` | place a widget only where absent (`omarchy bar put`) |
@@ -147,7 +148,7 @@ string on a miss.
"bar": { "bar": {
"id": "omarchy.bar", "id": "omarchy.bar",
"position": "top", "position": "top",
"transparent": false, "background": "opaque",
"centerAnchor": "omarchy.clock", "centerAnchor": "omarchy.clock",
"layout": { "layout": {
"left": [ { "id": "omarchy.menu" } ], "left": [ { "id": "omarchy.menu" } ],
+1 -1
View File
@@ -2,7 +2,7 @@
`omarchy-sudo-passwordless` publishes a timed or permanent grant for the numeric UID authenticated by sudo. Its user interface inspects sudo policy without prompting and authenticates only the enable action when access is off, without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`. `omarchy-sudo-passwordless` publishes a timed or permanent grant for the numeric UID authenticated by sudo. Its user interface inspects sudo policy without prompting and authenticates only the enable action when access is off, without a reusable sudo timestamp; fixed installed internal actions run as root and serialize on `/run/lock/omarchy-sudo-passwordless.lock`.
The menu bar's `PasswordlessSudo` indicator polls `--active` every five seconds and shows the passwordless sudo icon in red while access is active. The probe normally reads sudo's noninteractive long policy listing for the installed `__status` action and checks its `!authenticate` tag. If listing requires authentication, it falls back to a noninteractive `__status` call that ignores cached credentials. Neither path prompts or updates the credential cache. Interactive setup likewise falls back to noninteractive status and, when authentication is unavailable, leaves validation to the single confirmed enable action. Clicking the active indicator runs `--disable` noninteractively without opening a terminal, so a grant expiring between display and click cannot start the enable flow. The menu bar's `PasswordlessSudo` indicator runs `--active` when the shell starts and whenever `omarchy.indicators` is refreshed, and shows the passwordless sudo icon in red while access is active. The probe normally reads sudo's noninteractive long policy listing for the installed `__status` action and checks its `!authenticate` tag. If listing requires authentication, it falls back to a noninteractive `__status` call that ignores cached credentials. Neither path prompts or updates the credential cache. Interactive setup likewise falls back to noninteractive status and, when authentication is unavailable, leaves validation to the single confirmed enable action. Clicking the active indicator runs `--disable` noninteractively without opening a terminal, so a grant expiring between display and click cannot start the enable flow. The indicator never polls: every enable or disable attempt refreshes it directly, failed ones included since a rolled-back renewal can remove the previous grant, and enabling a timed grant also starts a transient user timer on the same wall-clock deadline that refreshes it two seconds later, so expiry during suspend shows on resume. The root expiry timer cannot reach the user's shell. If that refresh is missed, the indicator stays red past expiry, and clicking it reports access is already disabled and clears it.
## Grant lifecycle ## Grant lifecycle
+1 -1
View File
@@ -10,7 +10,7 @@ Beyond `colors.toml` and hand-written config overrides, a first-party theme can
A theme can pair a still background with a silent intro video using matching filenames: `backgrounds/0-winding-road.webp` and `backgrounds/intros/0-winding-road.mp4`. The video plays automatically at login and when switching to that theme with this background selected. No registration, checksum file or manual setting is needed. Supported video extensions are `mp4`, `m4v`, `mov`, `webm`, `mkv` and `avi`. Intro videos stay inside the nested `intros/` directory so the background picker does not list them as looping wallpapers. An image selected from another directory looks for its intro in that directory's own `intros/`, so a same-named custom image cannot inherit a theme's video. A theme can pair a still background with a silent intro video using matching filenames: `backgrounds/0-winding-road.webp` and `backgrounds/intros/0-winding-road.mp4`. The video plays automatically at login and when switching to that theme with this background selected. No registration, checksum file or manual setting is needed. Supported video extensions are `mp4`, `m4v`, `mov`, `webm`, `mkv` and `avi`. Intro videos stay inside the nested `intros/` directory so the background picker does not list them as looping wallpapers. An image selected from another directory looks for its intro in that directory's own `intros/`, so a same-named custom image cannot inherit a theme's video.
Aim for a video between five and seven seconds long that opens on its scene setup and ends on the matching still. The shell provides the opening crossfade, so an added fade from black is unnecessary. OWE owns decoding, mute and the handoff back to that image. The theme picker prepares the highlighted theme's remembered intro offscreen, paused at its opening frame. OWE keeps one prepared clip and an idle renderer for up to one minute, then releases them. A theme switch also starts preparation while its templates render; OWE compares the selected video with the prepared clip before resuming it. Older OWE versions use the ordinary startup path. Hyprland and the SDDM greeter use a black compositor background. On a fresh login, the persistent shell host keeps an opaque black cover above the bar and wallpaper until the bar has loaded and OWE has a moving video frame, then fades both in together over 420 ms. Hyprland initializes with a transparent cursor so its visibility polling cannot expose a pointer in the first frames. The temporary Xcursor theme stays private to the compositor; applications inherit the normal cursor environment. The opening fade restores the selected cursor and settings. Shell restarts and a fifteen-second compositor timeout recover a cursor left hidden by an interrupted startup. With intros disabled or unavailable, it waits for the still to be ready instead. A ten-second deadline releases the cover if a missing wallpaper or failed plugin never becomes ready. Ordinary shell restarts preload the selected still alongside the bar without mapping that login cover. Login intros use the Hyprland instance signature, so logging out and back in plays the intro again. The launcher consumes the current session before waiting up to five seconds for OWE. Shell restarts and a daemon starting after that window cannot play a delayed intro. Theme switches synchronize OWE with the selected image and use its first-frame start mode. An interrupted video supplies its currently playing frame as the outgoing snapshot. Rapid selections wait for the previous opening fade to finish before capturing it. Theme replacement and playback wait for the outgoing cover to decode and reach the compositor on every screen. A persistent layer holds that outgoing wallpaper until OWE hides the shell background and reports a ready video with an advancing playback position and no outgoing still transition, then crossfades it over 420 ms while applying the new bar palette. The shell retains its background service and decoded wallpaper while it is hidden, so handing the still back does not recreate them. App retints wait until the opening crossfade completes, with a three-second bound, so they do not compete with its first frames. The compositor reload waits until playback finishes because reloading Hyprland pauses video presentation. A superseded intro skips that reload. Failed playback releases the pending palette, and a newer theme invalidates the older handoff. Refreshing a theme, headless setup, and cycling backgrounds within the same theme do not trigger intros. Toggle > Animations suppresses playback along with the other animations. The `omarchy theme bg intro toggle` command can toggle intros separately. Aim for a video between five and seven seconds long that opens on its scene setup and ends on the matching still. The shell provides the opening crossfade, so an added fade from black is unnecessary. OWE owns decoding, mute and the handoff back to that image. The theme picker prepares the highlighted theme's remembered intro offscreen, paused at its opening frame. OWE keeps one prepared clip and an idle renderer for up to one minute, then releases them. A theme switch also starts preparation while its templates render; OWE compares the selected video with the prepared clip before resuming it. Older OWE versions use the ordinary startup path. Hyprland and the SDDM greeter use a black compositor background. On a fresh login, the persistent shell host keeps an opaque black cover above the bar and wallpaper until the bar has loaded and OWE has a moving video frame, then fades both in together over 420 ms. Hyprland initializes with a transparent cursor so its visibility polling cannot expose a pointer in the first frames. The temporary Xcursor theme stays private to the compositor; applications inherit the normal cursor environment. The opening fade restores the selected cursor and settings. Shell restarts and a fifteen-second compositor timeout recover a cursor left hidden by an interrupted startup. With intros disabled or unavailable, it waits for the still to be ready instead. A ten-second deadline releases the cover if a missing wallpaper or failed plugin never becomes ready. Ordinary shell restarts preload the selected still alongside the bar without mapping that login cover. Login intros use the Hyprland instance signature, so logging out and back in plays the intro again. The launcher consumes the current session before waiting up to five seconds for OWE. Shell restarts and a daemon starting after that window cannot play a delayed intro. Theme switches synchronize OWE with the selected image and use its first-frame start mode. An interrupted video supplies its currently playing frame as the outgoing snapshot. Rapid selections wait for the previous opening fade to finish before capturing it. Theme replacement and playback wait for the outgoing cover to decode and reach the compositor on every screen. A persistent layer holds that outgoing wallpaper until OWE hides the shell background and reports a ready video with an advancing playback position and no outgoing still transition, then crossfades it over 420 ms while applying the new bar palette. The shell retains its background service and decoded wallpaper while it is hidden, so handing the still back does not recreate them. App retints wait until the opening crossfade completes, with a three-second bound, so they do not compete with its first frames. The compositor reload waits until playback finishes because reloading Hyprland pauses video presentation. A superseded intro skips that reload. Failed playback releases the pending palette, and a newer theme invalidates the older handoff. Refreshing a theme, headless setup, and cycling backgrounds within the same theme do not trigger intros. Appearance > Animations suppresses playback along with the other animations. The `omarchy theme bg intro toggle` command can toggle intros separately.
A theme installed from a git repo is held to a much shorter list; see [What an installed theme may not ship](#what-an-installed-theme-may-not-ship). A theme installed from a git repo is held to a much shorter list; see [What an installed theme may not ship](#what-an-installed-theme-may-not-ship).
+6
View File
@@ -0,0 +1,6 @@
// Omarchy's own XKB option. libxkbcommon reads /etc/xkb before the system
// rules, which supply everything else.
! include %S/evdev
! option = symbols
omarchy:shift_caps_compose = +omarchy(shift_caps_compose)
+11
View File
@@ -0,0 +1,11 @@
// Caps Lock stays Caps Lock; Shift + Caps Lock is the Compose key. The actions
// are explicit so the Shift level never inherits the key's Lock modifier,
// which would toggle Caps Lock on every compose.
partial modifier_keys
xkb_symbols "shift_caps_compose" {
key <CAPS> {
type[Group1] = "TWO_LEVEL",
symbols[Group1] = [ Caps_Lock, Multi_key ],
actions[Group1] = [ LockMods(modifiers = Lock), NoAction() ]
};
};
+6 -5
View File
@@ -15,7 +15,6 @@ bolt
brightnessctl brightnessctl
btop btop
chromium chromium
clang
cliamp cliamp
cups cups
cups-filters cups-filters
@@ -34,7 +33,11 @@ eza
fakeroot fakeroot
fastfetch fastfetch
fcitx5 fcitx5
fcitx5-chewing
fcitx5-chinese-addons
fcitx5-gtk fcitx5-gtk
fcitx5-hangul
fcitx5-mozc
fcitx5-qt fcitx5-qt
fd fd
ffmpeg ffmpeg
@@ -66,7 +69,6 @@ inotify-tools
inxi inxi
networkmanager networkmanager
jq jq
kdenlive
kernel-modules-hook kernel-modules-hook
lazygit lazygit
less less
@@ -74,7 +76,6 @@ libsecret
libvips libvips
libyaml libyaml
libreoffice-fresh libreoffice-fresh
llvm
localsend localsend
lua51 lua51
luarocks luarocks
@@ -93,12 +94,12 @@ noto-fonts-cjk
noto-fonts-emoji noto-fonts-emoji
nss-mdns nss-mdns
nvim nvim
obs-studio
obsidian obsidian
omacalc omacalc
omacut omacut
omawrite omawrite
omarchy-nvim omarchy-nvim
omarchy-hyprland-titlebars
omasnap omasnap
# OWE owns desktop video backgrounds and feeds the lock screen its frames. # OWE owns desktop video backgrounds and feeds the lock screen its frames.
owe owe
@@ -125,6 +126,7 @@ quickshell
ripgrep ripgrep
ruby ruby
sddm sddm
signed
slurp slurp
socat socat
starship starship
@@ -157,7 +159,6 @@ woff2-font-awesome
xdg-desktop-portal-gtk xdg-desktop-portal-gtk
xdg-desktop-portal-hyprland xdg-desktop-portal-hyprland
xdg-terminal-exec xdg-terminal-exec
xournalpp
yaru-icon-theme yaru-icon-theme
yay yay
yt-dlp yt-dlp
+24 -5
View File
@@ -18,7 +18,8 @@
# so it never reaches the shell as SIGINT. Act on the status, never on a trap. # so it never reaches the shell as SIGINT. Act on the status, never on a trap.
# #
# Callers supply `notice <message> <seconds>` for validation feedback, and set # Callers supply `notice <message> <seconds>` for validation feedback, and set
# the variables these prompts write: keyboard, keyboard_label, username, # the variables these prompts write: keyboard, keyboard_label,
# keyboard_input_method, keyboard_xkb_layout, username,
# password, password_confirmation, full_name, email_address, hostname, timezone. # password, password_confirmation, full_name, email_address, hostname, timezone.
OMARCHY_FORM_BACK=1 OMARCHY_FORM_BACK=1
@@ -36,6 +37,8 @@ English (US, Colemak)|colemak
Belarusian|by Belarusian|by
Belgian|be-latin1 Belgian|be-latin1
Bulgarian|bg-cp1251 Bulgarian|bg-cp1251
Chinese (Simplified, Pinyin)|us|pinyin
Chinese (Traditional, Zhuyin)|us|chewing
Croatian|croat Croatian|croat
Czech|cz Czech|cz
Danish|dk-latin1 Danish|dk-latin1
@@ -54,10 +57,12 @@ Hungarian|hu
Icelandic|is-latin1 Icelandic|is-latin1
Irish|ie Irish|ie
Italian|it Italian|it
Japanese|jp106 Japanese|jp106|mozc
Japanese (US keyboard)|us|mozc
Kazakh|kazakh Kazakh|kazakh
Korean|us|hangul|kr
Kyrgyz|kyrgyz Kyrgyz|kyrgyz
Lao|la-latin1 Lao|us||la
Latvian|lv Latvian|lv
Lithuanian|lt Lithuanian|lt
Macedonian|mk-utf Macedonian|mk-utf
@@ -96,8 +101,22 @@ omarchy_prompt_keyboard() {
gum choose --height 10 --selected "English (US)" --header "Select keyboard layout") && status=0 || status=$? gum choose --height 10 --selected "English (US)" --header "Select keyboard layout") && status=0 || status=$?
((status == 0)) || return $status ((status == 0)) || return $status
keyboard_label="$choice" local selection
keyboard=$(printf '%s\n' "$OMARCHY_KEYBOARD_LAYOUTS" | awk -F'|' -v c="$choice" '$1==c{print $2; exit}') selection=$(printf '%s\n' "$OMARCHY_KEYBOARD_LAYOUTS" | awk -F'|' -v c="$choice" '$1==c{print; exit}')
[[ -n $selection ]] || return 1
IFS='|' read -r keyboard_label keyboard keyboard_input_method keyboard_xkb_layout <<< "$selection"
keyboard_input_method=${keyboard_input_method:-none}
keyboard_xkb_layout=${keyboard_xkb_layout:-}
}
# Root-side persistence. Console keymaps remain real keymaps; the input
# preference survives separately for every user of the installed system.
omarchy_persist_input_selection() {
local target_root=${1:-} method=${keyboard_input_method:-none} layout=${keyboard_xkb_layout:-}
case "$method" in none | mozc | hangul | pinyin | chewing) ;; *) return 1 ;; esac
case "$layout" in "" | kr | la) ;; *) return 1 ;; esac
mkdir -p "$target_root/etc/omarchy"
printf 'INPUT_METHOD=%s\nXKB_LAYOUT=%s\n' "$method" "$layout" > "$target_root/etc/omarchy/input-method"
} }
omarchy_prompt_username() { omarchy_prompt_username() {
+1
View File
@@ -2,6 +2,7 @@ run_logged "$OMARCHY_INSTALL/user/theme.sh"
run_logged "$OMARCHY_INSTALL/user/chromium.sh" run_logged "$OMARCHY_INSTALL/user/chromium.sh"
run_logged "$OMARCHY_INSTALL/user/git.sh" run_logged "$OMARCHY_INSTALL/user/git.sh"
run_logged "$OMARCHY_INSTALL/user/xcompose.sh" run_logged "$OMARCHY_INSTALL/user/xcompose.sh"
run_logged "$OMARCHY_INSTALL/user/input-method.sh"
run_logged "$OMARCHY_INSTALL/user/mise-work.sh" run_logged "$OMARCHY_INSTALL/user/mise-work.sh"
run_logged "$OMARCHY_INSTALL/user/hardware/asus/fix-audio-mixer.sh" run_logged "$OMARCHY_INSTALL/user/hardware/asus/fix-audio-mixer.sh"
@@ -11,6 +11,8 @@
set -euo pipefail set -euo pipefail
omarchy-input-method configure --defaults || echo "Input method defaults deferred; run omarchy setup input after login." >&2
systemctl --user daemon-reload systemctl --user daemon-reload
# One at a time: given a list, systemctl enables none of it when it cannot find # One at a time: given a list, systemctl enables none of it when it cannot find
+5 -3
View File
@@ -1,9 +1,11 @@
# A VM usually renders on the CPU, where animations and transparency cost # A VM usually renders on the CPU, where animations and transparency cost
# every frame, so start it without them. omarchy-toggle-animations brings them # every frame, so start it without them and with opaque windows.
# back. This runs before any Hyprland session exists, so place the flag # omarchy-toggle-animations and Appearance > Window Transparency bring them
# directly rather than through omarchy-hyprland-toggle, which reloads Hyprland. # back. This runs before any Hyprland session exists, so place the flags
# directly rather than through the commands, which reload Hyprland.
if omarchy-hw-vm; then if omarchy-hw-vm; then
echo "Detected a virtual machine. Turning off animations and transparency." echo "Detected a virtual machine. Turning off animations and transparency."
mkdir -p "$HOME/.local/state/omarchy/toggles/hypr" mkdir -p "$HOME/.local/state/omarchy/toggles/hypr"
cp "$OMARCHY_PATH/default/hypr/toggles/no-animations.lua" "$HOME/.local/state/omarchy/toggles/hypr/" cp "$OMARCHY_PATH/default/hypr/toggles/no-animations.lua" "$HOME/.local/state/omarchy/toggles/hypr/"
cp "$OMARCHY_PATH/default/hypr/toggles/window-opaque.lua" "$HOME/.local/state/omarchy/toggles/hypr/"
fi fi
+1
View File
@@ -0,0 +1 @@
omarchy-input-method configure --seed
+1 -1
View File
@@ -1,4 +1,4 @@
# Set default XCompose that is triggered with CapsLock # Set default XCompose that is triggered with Shift + CapsLock
tee ~/.XCompose >/dev/null <<EOF tee ~/.XCompose >/dev/null <<EOF
# Run omarchy-restart-xcompose to apply changes # Run omarchy-restart-xcompose to apply changes
+1 -1
View File
@@ -1,6 +1,6 @@
# Welcome to Omarchy! # Welcome to Omarchy!
Omarchy is an [omakase](https://manuals.omamix.org/3/omacom/76/omakase-computing) Linux distribution based on [Arch](https://archlinux.org/), the tiling window manager [Hyprland](https://hypr.land/), and the desktop construction-kit [Quickshell](https://quickshell.org/). It ships with everything a modern, savvy computer user needs to be productive immediately. From [Neovim](https://neovim.io/) (btw) to Chromium, [Obsidian](https://obsidian.md/) to LibreOffice, and Kdenlive to OBS Studio. Hell, even a retro Winamp-style music player is there! Omarchy is an [omakase](https://manuals.omamix.org/3/omacom/76/omakase-computing) Linux distribution based on [Arch](https://archlinux.org/), the tiling window manager [Hyprland](https://hypr.land/), and the desktop construction-kit [Quickshell](https://quickshell.org/). It ships with everything a modern, savvy computer user needs to be productive immediately. From [Neovim](https://neovim.io/) (btw) to Chromium, [Obsidian](https://obsidian.md/) to LibreOffice, and [Hype](https://github.com/omacom/hype) to [Monologue](https://github.com/omacom/monologue). Hell, even a retro Winamp-style music player is there!
This isn't just a grab bag of preinstalled packages, though. It's a complete system designed with both aesthetics and productivity in mind. Because a _beautiful_ system is a _motivating_ system, and productivity has always been [downstream from motivation](https://world.hey.com/dhh/beautiful-motivations-6fef7c73). There's zero bloat here: Just everything I use. This isn't just a grab bag of preinstalled packages, though. It's a complete system designed with both aesthetics and productivity in mind. Because a _beautiful_ system is a _motivating_ system, and productivity has always been [downstream from motivation](https://world.hey.com/dhh/beautiful-motivations-6fef7c73). There's zero bloat here: Just everything I use.
+9 -1
View File
@@ -34,9 +34,11 @@ Omarchy's default layout is called dwindle. It keeps all the windows you open on
But you can also choose to turn a workspace into the scrolling layout where windows are lined up side-by-side, beyond the visible edge of the display. You turn a single workspace into this layout via `Super + L`. But you can also choose to turn a workspace into the scrolling layout where windows are lined up side-by-side, beyond the visible edge of the display. You turn a single workspace into this layout via `Super + L`.
Pressing `Super + L` again turns the workspace floating, where windows overlap freely and you place them yourself, and a third press returns it to dwindle. `Super + Alt + L` goes through them the other way. A window that floats anyway — one you popped out with `Super + T`, or an app that always opens floating — keeps floating when the workspace goes back to tiling. The top bar marks the workspace you're on with its layout: a square for dwindle, a circle for scrolling, and a triangle for floating. Clicking that mark cycles the layout too.
![navigation-scrolling-layout](images/navigation-scrolling-layout.webp) ![navigation-scrolling-layout](images/navigation-scrolling-layout.webp)
The choice is per workspace, and it sticks. So you can keep workspace 1 on dwindle for browsing and workspace 2 on scrolling for code, and they'll come back that way after a restart. (The same toggle is under _Trigger > Toggle > Workspace Layout_ in the Omarchy menu). The choice is per workspace, and it sticks. So you can keep workspace 1 on dwindle for browsing and workspace 2 on scrolling for code, and they'll come back that way after a restart.
If you wish to use the scrolling layout as the default, you can set that in `~/.config/hypr/looknfeel.lua`: If you wish to use the scrolling layout as the default, you can set that in `~/.config/hypr/looknfeel.lua`:
@@ -48,6 +50,12 @@ hl.config({
}) })
``` ```
### Floating workspaces
A floating workspace works like a traditional desktop: windows overlap, and you move and size them yourself. Each window gets a titlebar in your theme's colors, with buttons to maximize and close. Click a window to bring it forward. Resize it from any edge or corner. Double-click a titlebar to maximize the window while keeping the top bar in view, and again to get its old size back.
New workspaces, and any you haven't given a layout of their own with `Super + L`, use the default layout. Pick it under _Trigger > Appearance > Workspace Layout_: Tiling, Scrolling, or Floating. `omarchy-hyprland-workspace-layout-default scrolling` sets it from a terminal.
### Grouping windows ### Grouping windows
Windows can be grouped using `Super + G`. Once you're in a group, every window you start while that's active will belong to the group. You can move between these grouped windows using `Super + Ctrl + Arrow Left/Right` or `Super + Alt + 1/2/3/4` to go directly to grouped window in order. Windows can be grouped using `Super + G`. Once you're in a group, every window you start while that's active will belong to the group. You can move between these grouped windows using `Super + Ctrl + Arrow Left/Right` or `Super + Alt + 1/2/3/4` to go directly to grouped window in order.
+6 -6
View File
@@ -6,7 +6,7 @@ It's also the one piece of the desktop that's always on screen, so it's worth kn
## What's on it by default ## What's on it by default
The bar has three sections. On the left sits the Omarchy logo (the menu launcher) and the workspace indicators. In the center you get the status indicators, the clock, the keyboard layout, the weather, and an Omarchy update badge. On the right: the system tray, agents, bluetooth, network, audio, display, and power. The bar has three sections. On the left sits the Omarchy logo (the menu launcher) and the workspace indicators. In the center you get the status indicators, the clock, Elsewhen (a world clock), the weather, and an Omarchy update badge. On the right: the keyboard layout (when more than one is configured), the system tray, agents, bluetooth, network, audio, display, and power.
A few of those only show up when they have something to say. The keyboard layout appears only if you've configured more than one layout. The update badge appears only when there's an Omarchy update waiting. And the agents icon appears the first time Omarchy finds AI coding usage on the machine (see [AI](17-ai.md)). A few of those only show up when they have something to say. The keyboard layout appears only if you've configured more than one layout. The update badge appears only when there's an Omarchy update waiting. And the agents icon appears the first time Omarchy finds AI coding usage on the machine (see [AI](17-ai.md)).
@@ -81,15 +81,15 @@ If you'd rather they were always visible, set `alwaysShow` to `true` on the widg
The bar configures itself. You don't have to open a config file to move things. The bar configures itself. You don't have to open a config file to move things.
Grab an empty patch of the bar around the center and drag it toward another screen edge, and the bar moves there — left, right, top, or bottom all work, and every widget adapts (vertical bars fall back to compact icon-only forms). A click-and-hold starts the same drag. Double-left-click that same empty space to toggle transparency. And drag any widget to reorder it or throw it into another section. Grab an empty patch of the bar around the center and drag it toward another screen edge, and the bar moves there — left, right, top, or bottom all work, and every widget adapts (vertical bars fall back to compact icon-only forms). A click-and-hold starts the same drag. Double-left-click that same empty space to cycle the bar background from clear to glassy to opaque. And drag any widget to reorder it or throw it into another section.
If you'd rather pick from a menu, **Style → Menu Bar** has both position and transparency. If you'd rather pick from a menu, **Trigger → Appearance → Menu Bar** has the position, and **Bar Background** picks how the bar is drawn: **Opaque Bar**, **Glassy Bar** (see-through, with what's behind it blurred), or **Clear Bar**.
The same things have commands, which is what you want for a [dotfiles](31-dotfiles.md) setup: The same things have commands, which is what you want for a [dotfiles](31-dotfiles.md) setup:
```bash ```bash
omarchy bar position bottom omarchy bar position bottom
omarchy bar transparent toggle omarchy bar background glassy # opaque, glassy, none, or cycle
omarchy bar move omarchy.clock --section center --index 0 omarchy bar move omarchy.clock --section center --index 0
omarchy bar set omarchy.clock format "HH:mm" omarchy bar set omarchy.clock format "HH:mm"
omarchy bar defaults # back to the shipped layout omarchy bar defaults # back to the shipped layout
@@ -104,7 +104,7 @@ omarchy plugin disable omarchy.weather
## Hiding the bar ## Hiding the bar
`Super + Shift + Space` toggles the bar off and back on without killing the shell — panels and hotkeys keep working, you just get the pixels back. It's also in the menu under **Trigger → Toggle → Menu Bar**. `Super + Shift + Space` toggles the bar off and back on without killing the shell — panels and hotkeys keep working, you just get the pixels back. It's also in the menu under **Trigger → Appearance → Menu Bar → Hide Bar**.
## The config file ## The config file
@@ -115,7 +115,7 @@ All of it is stored in `~/.config/omarchy/shell.json`, under the `bar` key. Here
"version": 1, "version": 1,
"bar": { "bar": {
"position": "top", "position": "top",
"transparent": false, "background": "opaque",
"centerAnchor": "omarchy.clock", "centerAnchor": "omarchy.clock",
"layout": { "layout": {
"left": [{ "id": "omarchy.menu" }, { "id": "omarchy.workspaces" }], "left": [{ "id": "omarchy.menu" }, { "id": "omarchy.workspaces" }],
+28 -27
View File
@@ -15,7 +15,8 @@ You can see all the main keyboard bindings with `Super + K` (Tmux bindings with
| `Super + T` | Toggle window between tiling/floating | | `Super + T` | Toggle window between tiling/floating |
| `Super + J` | Toggle window position (horizontal/vertical) | | `Super + J` | Toggle window position (horizontal/vertical) |
| `Super + O` | Toggle popping window into sticky'n'floating | | `Super + O` | Toggle popping window into sticky'n'floating |
| `Super + L` | Toggle between dwindle and scrolling layout | | `Super + L` | Cycle through the dwindle, scrolling, and floating layouts |
| `Super + Alt + L` | Cycle through the layouts in reverse |
| `Super + P` | Toggle pseudo window style (natural v stretch) | | `Super + P` | Toggle pseudo window style (natural v stretch) |
| `Super + F` | Go full screen | | `Super + F` | Go full screen |
| `Super + Alt + F` | Go full width | | `Super + Alt + F` | Go full width |
@@ -339,36 +340,36 @@ You can use `Super + Ctrl + E` to show a complete emoji picker that'll put the s
| Hotkey | EM | Clue | | Hotkey | EM | Clue |
| ------------ | -- | ---------- | | ------------ | -- | ---------- |
| `CapsLock M S` | 😄 | smile | | `Shift + CapsLock M S` | 😄 | smile |
| `CapsLock M C` | 😂 | cry | | `Shift + CapsLock M C` | 😂 | cry |
| `CapsLock M L` | 😍 | love | | `Shift + CapsLock M L` | 😍 | love |
| `CapsLock M V` | ✌️ | victory | | `Shift + CapsLock M V` | ✌️ | victory |
| `CapsLock M H` | ❤️ | heart | | `Shift + CapsLock M H` | ❤️ | heart |
| `CapsLock M Y` | 👍 | yes | | `Shift + CapsLock M Y` | 👍 | yes |
| `CapsLock M N` | 👎 | no | | `Shift + CapsLock M N` | 👎 | no |
| `CapsLock M F` | 🖕 | fuck | | `Shift + CapsLock M F` | 🖕 | fuck |
| `CapsLock M W` | 🤞 | wish | | `Shift + CapsLock M W` | 🤞 | wish |
| `CapsLock M R` | 🤘 | rock | | `Shift + CapsLock M R` | 🤘 | rock |
| `CapsLock M K` | 😘 | kiss | | `Shift + CapsLock M K` | 😘 | kiss |
| `CapsLock M E` | 🙄 | eyeroll | | `Shift + CapsLock M E` | 🙄 | eyeroll |
| `CapsLock M I` | 😉 | wink | | `Shift + CapsLock M I` | 😉 | wink |
| `CapsLock M P` | 🙏 | pray | | `Shift + CapsLock M P` | 🙏 | pray |
| `CapsLock M D` | 🤤 | drool | | `Shift + CapsLock M D` | 🤤 | drool |
| `CapsLock M M` | 💰 | money | | `Shift + CapsLock M M` | 💰 | money |
| `CapsLock M X` | 🎉 | xellebrate | | `Shift + CapsLock M X` | 🎉 | xellebrate |
| `CapsLock M 1` | 💯 | 100% | | `Shift + CapsLock M 1` | 💯 | 100% |
| `CapsLock M T` | 🥂 | toast | | `Shift + CapsLock M T` | 🥂 | toast |
| `CapsLock M O` |👌 | ok | | `Shift + CapsLock M O` |👌 | ok |
| `CapsLock M G` |👋 | greeting | | `Shift + CapsLock M G` |👋 | greeting |
| `CapsLock M A` |💪 | arm | | `Shift + CapsLock M A` |💪 | arm |
| `CapsLock M B` |🤯 | blowing | | `Shift + CapsLock M B` |🤯 | blowing |
## Quick Completions ## Quick Completions
| Hotkey | Completion | | Hotkey | Completion |
| ------------ | ---------- | | ------------ | ---------- |
| `CapsLock Space Space` | — (mdash) | | `Shift + CapsLock Space Space` | — (mdash) |
| `CapsLock Space N` | Your name (as entered on setup) | | `Shift + CapsLock Space N` | Your name (as entered on setup) |
| `CapsLock Space E` | Your email (as entered on setup) | | `Shift + CapsLock Space E` | Your email (as entered on setup) |
You can add more of your own by editing `~/.XCompose`, then running `omarchy-restart-xcompose` in the terminal to get the changes picked up. You can add more of your own by editing `~/.XCompose`, then running `omarchy-restart-xcompose` in the terminal to get the changes picked up.
+2 -2
View File
@@ -4,7 +4,7 @@ A lot of what you change day to day isn't really a setting. It's a mode you flip
### The toggle menu ### The toggle menu
`Super + Ctrl + O` opens _Trigger > Toggle_ directly, or you can walk there from the Omarchy menu (`Super + Space`). Everything in that list is a switch you can flip without thinking about where the state lives. `Super + Ctrl + O` opens _Trigger > Toggle_ directly, or you can walk there from the Omarchy menu (`Super + Space`). Everything in that list is a switch you can flip without thinking about where the state lives. Switches for how the desktop looks sit next to it, under _Trigger > Appearance_.
From the terminal, the same switches are `omarchy toggle <thing>`. Run `omarchy toggle` on its own to see the whole group. From the terminal, the same switches are `omarchy toggle <thing>`. Run `omarchy toggle` on its own to see the whole group.
@@ -24,7 +24,7 @@ From the terminal, the same switches are `omarchy toggle <thing>`. Run `omarchy
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 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`). _Trigger > Appearance_ holds the menu bar and animations, plus a few things that aren't `omarchy toggle` commands but behave the same: battery percentage in the bar, the default workspace layout, window gaps (`Super + Shift + Backspace`), and the 1-window square aspect (`Super + Ctrl + Backspace`).
Most of these are just a flag file under `~/.local/state/omarchy/toggles/`. If you want to branch on one in a script, `omarchy-toggle-enabled` gives you an exit code instead of making you go looking: Most of these are just a flag file under `~/.local/state/omarchy/toggles/`. If you want to branch on one in a script, `omarchy-toggle-enabled` gives you an exit code instead of making you go looking:
+6 -12
View File
@@ -69,6 +69,12 @@ You start LibreOffice via the application launcher (`Super + Space`).
You start Hype via the application launcher (`Super + Space`). Press `?` inside it to see every shortcut. You start Hype via the application launcher (`Super + Space`). Press `?` inside it to see every shortcut.
## Signed
[Signed](https://github.com/omacom/signed) is Omarchy's own dead-simple way to sign PDFs and fill them in. Put your saved signature, text, and today's date anywhere on a page, then save a signed copy beside the original. See [Filling out PDFs](27-filling-out-pdfs.md) for how.
You start Signed via the application launcher (`Super + Space`), or open a PDF with it from the file manager.
## Omacalc ## Omacalc
[Omacalc](https://github.com/omacom-io/omacalc) is Omarchy's own dead-simple calculator, which opens in a floating window. [Omacalc](https://github.com/omacom-io/omacalc) is Omarchy's own dead-simple calculator, which opens in a floating window.
@@ -87,18 +93,6 @@ You start Signal with `Super + Shift + G`. It's not part of the base install, so
You start mpv via the application launcher (`Super + Space`) or just double-click on a video in the file manager. You start mpv via the application launcher (`Super + Space`) or just double-click on a video in the file manager.
## OBS Studio
[OBS Studio](https://obsproject.com/) lets you record or stream video from multiple inputs. You can mix a screencast with a webcam with a microphone input. It's what was used to record the Omarchy screencasts.
You start OBS Studio via the application launcher (`Super + Space`).
## Kdenlive
[Kdenlive](https://kdenlive.org/) is an excellent video editor. Perfect for working on video that comes out of OBS Studio before sharing it.
You start Kdenlive via the application launcher (`Super + Space`).
## Omacut ## Omacut
[Omacut](https://github.com/omacom-io/omacut) is Omarchy's own dead-simple video trimmer. When all you need is to cut the start and end off a clip, it beats firing up a full video editor. [Omacut](https://github.com/omacom-io/omacut) is Omarchy's own dead-simple video trimmer. When all you need is to cut the start and end off a clip, it beats firing up a full video editor.
Loaded 100 of 223 files, more files were not shown because too many files have changed in this diff. Show more