Compare commits
112
Commits
v4.0.0-beta3
...
v4.0.1
No files matched your search
@@ -6,6 +6,7 @@ matching guide before starting:
|
|||||||
- [`agents/skills/command-metadata.md`](agents/skills/command-metadata.md) - adding or changing commands in `bin/`
|
- [`agents/skills/command-metadata.md`](agents/skills/command-metadata.md) - adding or changing commands in `bin/`
|
||||||
- [`agents/skills/install-scripts.md`](agents/skills/install-scripts.md) - working under `install/` or on system/user setup commands
|
- [`agents/skills/install-scripts.md`](agents/skills/install-scripts.md) - working under `install/` or on system/user setup commands
|
||||||
- [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) - editing the Quickshell desktop under `shell/`
|
- [`agents/skills/shell-dev.md`](agents/skills/shell-dev.md) - editing the Quickshell desktop under `shell/`
|
||||||
|
- [`agents/skills/icon-font.md`](agents/skills/icon-font.md) - adding branded glyphs to `default/fonts/omarchy/omarchy.ttf`
|
||||||
- [`agents/skills/acceptance-tests.md`](agents/skills/acceptance-tests.md) - writing or running graphical acceptance tests under `test/acceptance.d/`
|
- [`agents/skills/acceptance-tests.md`](agents/skills/acceptance-tests.md) - writing or running graphical acceptance tests under `test/acceptance.d/`
|
||||||
- [`agents/skills/visual-verification.md`](agents/skills/visual-verification.md) - verifying any change with a visual effect in the running UI
|
- [`agents/skills/visual-verification.md`](agents/skills/visual-verification.md) - verifying any change with a visual effect in the running UI
|
||||||
- [`docs/migrations.md`](docs/migrations.md) - creating or changing migrations under `migrations/`
|
- [`docs/migrations.md`](docs/migrations.md) - creating or changing migrations under `migrations/`
|
||||||
|
|||||||
@@ -4,6 +4,76 @@ Omarchy is a beautiful, modern & opinionated Linux distribution by DHH.
|
|||||||
|
|
||||||
Read more at [omarchy.org](https://omarchy.org).
|
Read more at [omarchy.org](https://omarchy.org).
|
||||||
|
|
||||||
|
## The Omarchy Manual
|
||||||
|
|
||||||
|
The manual lives in [`manual/`](manual/), which is its authoritative source. It's
|
||||||
|
mirrored to [learn.omacom.io](https://learn.omacom.io/2/the-omarchy-manual), where
|
||||||
|
its screenshots are also hosted.
|
||||||
|
|
||||||
|
- [Welcome to Omarchy!](manual/01-welcome-to-omarchy.md)
|
||||||
|
|
||||||
|
**The Basics**
|
||||||
|
|
||||||
|
- [Getting Started](manual/02-getting-started.md)
|
||||||
|
- [Coming From Mac or Windows](manual/03-coming-from-mac-or-windows.md)
|
||||||
|
- [Navigation](manual/04-navigation.md)
|
||||||
|
- [The top bar](manual/05-the-top-bar.md)
|
||||||
|
- [Themes](manual/06-themes.md)
|
||||||
|
- [Hotkeys](manual/07-hotkeys.md)
|
||||||
|
- [Unified Clipboard & History](manual/08-unified-clipboard-history.md)
|
||||||
|
- [Reminders](manual/09-reminders.md)
|
||||||
|
- [Notices](manual/10-notices.md)
|
||||||
|
- [Text Extraction & Dictation](manual/11-text-extraction-dictation.md)
|
||||||
|
- [Screenshots & Recording](manual/12-screenshots-recording.md)
|
||||||
|
- [Toggles, idle & screensaver](manual/13-toggles-idle-screensaver.md)
|
||||||
|
- [Omarchy CLI](manual/14-omarchy-cli.md)
|
||||||
|
|
||||||
|
**The Applications**
|
||||||
|
|
||||||
|
- [Terminal](manual/15-terminal.md)
|
||||||
|
- [Neovim](manual/16-neovim.md)
|
||||||
|
- [AI](manual/17-ai.md)
|
||||||
|
- [Development Tools](manual/18-development-tools.md)
|
||||||
|
- [Shell Tools](manual/19-shell-tools.md)
|
||||||
|
- [Shell Functions](manual/20-shell-functions.md)
|
||||||
|
- [TUIs](manual/21-tuis.md)
|
||||||
|
- [GUIs](manual/22-guis.md)
|
||||||
|
- [Browsers](manual/23-browsers.md)
|
||||||
|
- [Commercial apps/services](manual/24-commercial-apps-services.md)
|
||||||
|
- [Web Apps](manual/25-web-apps.md)
|
||||||
|
- [Gaming](manual/26-gaming.md)
|
||||||
|
- [Filling out PDFs](manual/27-filling-out-pdfs.md)
|
||||||
|
- [Windows VM](manual/28-windows-vm.md)
|
||||||
|
- [Other Packages](manual/29-other-packages.md)
|
||||||
|
|
||||||
|
**Configuration**
|
||||||
|
|
||||||
|
- [Updates](manual/30-updates.md)
|
||||||
|
- [Dotfiles](manual/31-dotfiles.md)
|
||||||
|
- [Shell plugins](manual/32-shell-plugins.md)
|
||||||
|
- [Monitors](manual/33-monitors.md)
|
||||||
|
- [Keyboard, Mouse, Trackpad](manual/34-keyboard-mouse-trackpad.md)
|
||||||
|
- [Networking](manual/35-networking.md)
|
||||||
|
- [System sleep](manual/36-system-sleep.md)
|
||||||
|
- [Hardware authentication](manual/37-hardware-authentication.md)
|
||||||
|
- [Fonts](manual/38-fonts.md)
|
||||||
|
- [Backgrounds](manual/39-backgrounds.md)
|
||||||
|
- [Prompt](manual/40-prompt.md)
|
||||||
|
- [Branding](manual/41-branding.md)
|
||||||
|
- [Common tweaks](manual/42-common-tweaks.md)
|
||||||
|
- [Making your own theme](manual/43-making-your-own-theme.md)
|
||||||
|
|
||||||
|
**The Rest**
|
||||||
|
|
||||||
|
- [Mac support](manual/44-mac-support.md)
|
||||||
|
- [Troubleshooting](manual/45-troubleshooting.md)
|
||||||
|
- [FAQ](manual/46-faq.md)
|
||||||
|
- [System snapshots](manual/47-system-snapshots.md)
|
||||||
|
- [Security](manual/48-security.md)
|
||||||
|
- [Omarchy on...](manual/49-omarchy-on.md)
|
||||||
|
- [Dual Boot Install](manual/50-dual-boot-install.md)
|
||||||
|
- [Unattended Installs](manual/51-unattended-installs.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Omarchy is released under the [MIT License](https://opensource.org/licenses/MIT).
|
Omarchy is released under the [MIT License](https://opensource.org/licenses/MIT).
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Omarchy Icon Font
|
||||||
|
|
||||||
|
Read this before adding a branded glyph to `default/fonts/omarchy/omarchy.ttf`.
|
||||||
|
|
||||||
|
The Omarchy icon font is a small private-use font carrying the marks Nerd
|
||||||
|
Fonts does not have: the Omarchy logo and the agent and app brand marks. The
|
||||||
|
menu draws one by naming the font on an entry:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok", ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `iconFont`, an entry's `icon` is drawn in the menu font, so reach for
|
||||||
|
this font only when a Nerd Font glyph would misrepresent the thing. A generic
|
||||||
|
robot for four different AI apps is the case that justifies a real mark; a
|
||||||
|
folder or a microphone is not.
|
||||||
|
|
||||||
|
`default/fonts/omarchy/README.md` lists every glyph with the URL its artwork
|
||||||
|
came from. Keep that list accurate — it is the only record of provenance.
|
||||||
|
|
||||||
|
## Adding a glyph
|
||||||
|
|
||||||
|
`omarchy dev font` does the work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
omarchy dev font list
|
||||||
|
omarchy dev font add ollama https://simpleicons.org/icons/ollama.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
`add` fetches the SVG, scales it into the same 64..960 box the existing marks
|
||||||
|
use so it lands at their optical size, appends it at the next free private-use
|
||||||
|
codepoint, and adds a line to the font README. It prints the codepoint and the
|
||||||
|
glyph itself.
|
||||||
|
|
||||||
|
The source must be a **monochrome SVG with a single `<path>`**, because the
|
||||||
|
menu recolors the glyph with the active theme's foreground and selection
|
||||||
|
colors. Brand icon sets such as <https://simpleicons.org> publish exactly that
|
||||||
|
shape. App favicons usually do not: they are multi-color, carry a container
|
||||||
|
tile, or split across several paths. Prefer the official mark when it is
|
||||||
|
published as flat monochrome art, and fall back to an icon set's redraw when it
|
||||||
|
is not.
|
||||||
|
|
||||||
|
Two-tone marks are a trap. Every path becomes solid foreground, so a logo whose
|
||||||
|
meaning depends on lighter and darker halves turns into an unreadable blob.
|
||||||
|
Pick a source whose silhouette alone reads.
|
||||||
|
|
||||||
|
## After adding
|
||||||
|
|
||||||
|
The command prints these, and all of them matter:
|
||||||
|
|
||||||
|
- Point the menu entry at the new codepoint with `"iconFont":"omarchy"`.
|
||||||
|
- Bump the charset range asserted in `test/shell.d/menu-test.sh`; that test
|
||||||
|
pins the font's coverage and fails until it matches.
|
||||||
|
- Check the README line the command added, and give it a proper display name
|
||||||
|
with `--label` if the glyph name is not the brand's name.
|
||||||
|
|
||||||
|
The font is package-owned: `omarchy-settings` installs it to
|
||||||
|
`/usr/share/fonts/omarchy/omarchy.ttf`, so a new glyph reaches the desktop
|
||||||
|
through a settings release, not through `omarchy update`. Between the merge and
|
||||||
|
that release, a pulled checkout renders the new entry with no icon.
|
||||||
|
|
||||||
|
## Verifying
|
||||||
|
|
||||||
|
Render the whole font and look at it, which catches inverted contours and
|
||||||
|
filled counters that a glyph list cannot show:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "open('/tmp/row.txt','w').write(' '.join(chr(c) for c in range(0xE900, 0xE910)))"
|
||||||
|
magick -background white -fill black -font default/fonts/omarchy/omarchy.ttf \
|
||||||
|
-pointsize 110 label:@/tmp/row.txt /tmp/font-row.png
|
||||||
|
```
|
||||||
|
|
||||||
|
Then confirm it in the running menu per
|
||||||
|
[`visual-verification.md`](visual-verification.md). Fontconfig prefers the
|
||||||
|
packaged font over a copy in `~/.local/share/fonts` for the same family, so a
|
||||||
|
preview needs either the real file replaced or a `<rejectfont>` rule in
|
||||||
|
`~/.config/fontconfig/conf.d/` pointing fontconfig away from the packaged one.
|
||||||
|
Restart the shell afterwards — Qt reads the font database at startup, so
|
||||||
|
`omarchy menu refresh` alone will not pick up a changed font.
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Version=1.0
|
|
||||||
Name=ChatGPT
|
|
||||||
Exec=omarchy-launch-webapp https://chatgpt.com/
|
|
||||||
Terminal=false
|
|
||||||
Type=Application
|
|
||||||
Icon=chatgpt
|
|
||||||
StartupNotify=true
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Name=Docker
|
Name=Docker
|
||||||
Exec=xdg-terminal-exec --app-id=TUI.tile -e lazydocker
|
Exec=xdg-terminal-exec --app-id=TUI.tile -e omarchy-launch-docker-tui
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=docker
|
Icon=docker
|
||||||
|
|||||||
+29
-6
@@ -78,7 +78,6 @@ GROUP_DESCRIPTIONS[setup]="Interactive setup wizards"
|
|||||||
GROUP_DESCRIPTIONS[shell]="Omarchy shell IPC helpers"
|
GROUP_DESCRIPTIONS[shell]="Omarchy shell IPC helpers"
|
||||||
GROUP_DESCRIPTIONS[screensaver]="Screensaver branding and animation"
|
GROUP_DESCRIPTIONS[screensaver]="Screensaver branding and animation"
|
||||||
GROUP_DESCRIPTIONS[snapshot]="System snapshots"
|
GROUP_DESCRIPTIONS[snapshot]="System snapshots"
|
||||||
GROUP_DESCRIPTIONS[style]="Global UI style controls"
|
|
||||||
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
|
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
|
||||||
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
|
||||||
GROUP_DESCRIPTIONS[tailscale]="Tailscale helpers"
|
GROUP_DESCRIPTIONS[tailscale]="Tailscale helpers"
|
||||||
@@ -86,7 +85,6 @@ GROUP_DESCRIPTIONS[theme]="Theme management"
|
|||||||
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
|
||||||
GROUP_DESCRIPTIONS[transcode]="Image and video transcoding"
|
GROUP_DESCRIPTIONS[transcode]="Image and video transcoding"
|
||||||
GROUP_DESCRIPTIONS[tui]="Terminal UI launchers"
|
GROUP_DESCRIPTIONS[tui]="Terminal UI launchers"
|
||||||
GROUP_DESCRIPTIONS[tz]="Timezone selection"
|
|
||||||
GROUP_DESCRIPTIONS[update]="Omarchy and system updates"
|
GROUP_DESCRIPTIONS[update]="Omarchy and system updates"
|
||||||
GROUP_DESCRIPTIONS[version]="Version and channel information"
|
GROUP_DESCRIPTIONS[version]="Version and channel information"
|
||||||
GROUP_DESCRIPTIONS[voxtype]="Voxtype dictation"
|
GROUP_DESCRIPTIONS[voxtype]="Voxtype dictation"
|
||||||
@@ -122,6 +120,31 @@ append_pipe_value() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A route can resolve to a binary before all args are consumed (`update aur` only matches the `update` binary, leaving `aur` and `--help` as leftovers)
|
||||||
|
# --help must never be lost among those leftovers and get forwarded into the real command, so this checks the whole remainder rather than just the first token.
|
||||||
|
# A `--` marks everything after it as belonging to the command itself, so scanning stops there.
|
||||||
|
remaining_has_help_flag() {
|
||||||
|
local token=""
|
||||||
|
|
||||||
|
for token in "$@"; do
|
||||||
|
[[ $token == "--" ]] && break
|
||||||
|
[[ $token == "--help" || $token == "-h" ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining_has_json_flag() {
|
||||||
|
local token=""
|
||||||
|
|
||||||
|
for token in "$@"; do
|
||||||
|
[[ $token == "--" ]] && break
|
||||||
|
[[ $token == "--json" ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
register_route() {
|
register_route() {
|
||||||
local route="$1"
|
local route="$1"
|
||||||
local key="$2"
|
local key="$2"
|
||||||
@@ -930,7 +953,7 @@ dispatch_fast_or_help() {
|
|||||||
remaining=("${args[@]:DIRECT_RESOLVED_COUNT}")
|
remaining=("${args[@]:DIRECT_RESOLVED_COUNT}")
|
||||||
binary_path="$OMARCHY_BIN_DIR/$DIRECT_RESOLVED_BINARY"
|
binary_path="$OMARCHY_BIN_DIR/$DIRECT_RESOLVED_BINARY"
|
||||||
|
|
||||||
if (( ${#remaining[@]} > 0 )) && [[ ${remaining[0]} == "--help" || ${remaining[0]} == "-h" ]]; then
|
if remaining_has_help_flag "${remaining[@]}"; then
|
||||||
if ! load_command_by_binary "$DIRECT_RESOLVED_BINARY"; then
|
if ! load_command_by_binary "$DIRECT_RESOLVED_BINARY"; then
|
||||||
echo "Binary is missing or not executable: $DIRECT_RESOLVED_BINARY" >&2
|
echo "Binary is missing or not executable: $DIRECT_RESOLVED_BINARY" >&2
|
||||||
return 127
|
return 127
|
||||||
@@ -941,7 +964,7 @@ dispatch_fast_or_help() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
key="${BINARY_TO_KEY[$DIRECT_RESOLVED_BINARY]}"
|
key="${BINARY_TO_KEY[$DIRECT_RESOLVED_BINARY]}"
|
||||||
if [[ " ${remaining[*]} " == *" --json "* ]]; then
|
if remaining_has_json_flag "${remaining[@]}"; then
|
||||||
show_command_json "$key"
|
show_command_json "$key"
|
||||||
else
|
else
|
||||||
show_command_help "$key"
|
show_command_help "$key"
|
||||||
@@ -994,8 +1017,8 @@ dispatch_or_help() {
|
|||||||
key="$RESOLVED_KEY"
|
key="$RESOLVED_KEY"
|
||||||
remaining=("${args[@]:RESOLVED_COUNT}")
|
remaining=("${args[@]:RESOLVED_COUNT}")
|
||||||
|
|
||||||
if (( ${#remaining[@]} > 0 )) && [[ ${remaining[0]} == "--help" || ${remaining[0]} == "-h" ]]; then
|
if remaining_has_help_flag "${remaining[@]}"; then
|
||||||
if [[ " ${remaining[*]} " == *" --json "* ]]; then
|
if remaining_has_json_flag "${remaining[@]}"; then
|
||||||
show_command_json "$key"
|
show_command_json "$key"
|
||||||
else
|
else
|
||||||
show_command_help "$key"
|
show_command_help "$key"
|
||||||
|
|||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Launch the default coding agent in a terminal
|
||||||
|
# omarchy:args=[--inline] [--pick]
|
||||||
|
# omarchy:examples=omarchy agent | omarchy agent --inline
|
||||||
|
|
||||||
|
inline=false
|
||||||
|
pick=false
|
||||||
|
|
||||||
|
# Flags only. A bare prompt would shadow the subcommands under this group, so
|
||||||
|
# prompts go through omarchy-agent-prompt, which passes one here as --prompt.
|
||||||
|
while (($#)); do
|
||||||
|
case "$1" in
|
||||||
|
--inline)
|
||||||
|
inline=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--pick)
|
||||||
|
pick=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--prompt)
|
||||||
|
prompt=${2:?--prompt needs a value}
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unexpected argument: $1" >&2
|
||||||
|
echo "To pass a prompt: omarchy agent prompt \"$*\"" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Agents refuse to remember trust for $HOME, so launches from the keybinding or
|
||||||
|
# menu start in the work directory instead of re-asking on every session.
|
||||||
|
[[ $PWD == "$HOME" && -d $HOME/Work ]] && cd "$HOME/Work"
|
||||||
|
|
||||||
|
agent=$(omarchy-default-agent)
|
||||||
|
|
||||||
|
# Omarchy ships without a default, so there is nothing to launch until one is
|
||||||
|
# picked. --pick offers the choice instead of the error, which is what the
|
||||||
|
# keybinding wants: a keypress that opens nothing explains nothing.
|
||||||
|
if [[ -z $agent ]]; then
|
||||||
|
[[ $pick == "true" ]] && exec omarchy-menu summon setup.default.agent
|
||||||
|
echo "Choose default agent with: omarchy default agent <name>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if omarchy-cmd-missing "$agent"; then
|
||||||
|
echo "$agent is not installed. Choose an installed agent with: omarchy default agent <name>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Agents launched from the keybinding or menu run unattended, so each one starts
|
||||||
|
# with its own spelling of "don't stop to ask". Pi has no approval prompts.
|
||||||
|
case "$agent" in
|
||||||
|
opencode)
|
||||||
|
command=(opencode --auto)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(--prompt "$prompt")
|
||||||
|
;;
|
||||||
|
gemini)
|
||||||
|
command=(gemini --yolo)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(--prompt-interactive "$prompt")
|
||||||
|
;;
|
||||||
|
copilot)
|
||||||
|
command=(copilot --allow-all)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(--interactive "$prompt")
|
||||||
|
;;
|
||||||
|
crush)
|
||||||
|
# --yolo belongs to the interactive command only; `crush run` never prompts.
|
||||||
|
if [[ -n ${prompt:-} ]]; then
|
||||||
|
command=(crush run "$prompt")
|
||||||
|
else
|
||||||
|
command=(crush --yolo)
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
claude)
|
||||||
|
command=(claude --permission-mode auto)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||||
|
;;
|
||||||
|
grok)
|
||||||
|
command=(grok --permission-mode bypassPermissions)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||||
|
;;
|
||||||
|
codex)
|
||||||
|
command=(codex --approve-for-me)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||||
|
;;
|
||||||
|
omp)
|
||||||
|
command=(omp --auto-approve)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||||
|
;;
|
||||||
|
pi)
|
||||||
|
command=(pi)
|
||||||
|
[[ -n ${prompt:-} ]] && command+=("$prompt")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported default agent: $agent" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ $inline == "true" ]]; then
|
||||||
|
exec "${command[@]}"
|
||||||
|
else
|
||||||
|
# A fixed app-id rather than the default org.omarchy.<binary>, so every agent
|
||||||
|
# window shares one class for window rules and themes to single out.
|
||||||
|
exec omarchy-launch-tui --app-id=org.omarchy.agent "${command[@]}"
|
||||||
|
fi
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Diagnose a crashed process with the default coding agent
|
||||||
|
# omarchy:args=<pid> [comm] [exe] [signal]
|
||||||
|
# omarchy:examples=omarchy agent crash 1516893
|
||||||
|
|
||||||
|
# Clicked from a "Process crashed:" notification, or run by hand against any PID
|
||||||
|
# in `coredumpctl list`. The method lives in the diagnose-crash skill so it is
|
||||||
|
# edited in one place and works with whichever agent is default; this only
|
||||||
|
# gathers the facts and points at it.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
pid=${1:?usage: omarchy-agent-crash <pid> [comm] [exe] [signal]}
|
||||||
|
|
||||||
|
if [[ ! $pid =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Not a PID: $pid" >&2
|
||||||
|
echo "Usage: omarchy agent crash <pid> (see: coredumpctl list)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
comm=${2:-unknown}
|
||||||
|
exe=${3:-unknown}
|
||||||
|
signal=${4:-unknown}
|
||||||
|
|
||||||
|
skill="$OMARCHY_PATH/default/agents/skills/diagnose-crash/SKILL.md"
|
||||||
|
|
||||||
|
# Looked up live so a hand-run PID still gets a timestamp. A rotated-away core
|
||||||
|
# only costs the timestamp, so failure is tolerated.
|
||||||
|
when=$(coredumpctl list "$pid" --no-pager --no-legend 2>/dev/null | tail -1 | cut -d' ' -f1-4) || true
|
||||||
|
when=${when:-unknown}
|
||||||
|
|
||||||
|
prompt=$(
|
||||||
|
cat <<PROMPT
|
||||||
|
A process crashed on this Omarchy machine and I want to know why.
|
||||||
|
|
||||||
|
What systemd-coredump recorded:
|
||||||
|
process: $comm
|
||||||
|
PID: $pid
|
||||||
|
binary: $exe
|
||||||
|
signal: $signal
|
||||||
|
time: $when
|
||||||
|
|
||||||
|
Use the diagnose-crash skill: it covers how to investigate, what to report, and
|
||||||
|
when a crash is worth reporting upstream to Omarchy. If your harness has no skill
|
||||||
|
mechanism, read the skill files directly and follow them instead:
|
||||||
|
|
||||||
|
$skill
|
||||||
|
PROMPT
|
||||||
|
)
|
||||||
|
|
||||||
|
exec omarchy-agent --prompt "$prompt"
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Launch the default coding agent with a prompt
|
||||||
|
# omarchy:args=[--inline] <prompt...>
|
||||||
|
# omarchy:examples=omarchy agent prompt "Review this project"
|
||||||
|
|
||||||
|
# Prompts live here rather than on `omarchy agent`, where a bare prompt would
|
||||||
|
# shadow the subcommands under that group.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
inline=()
|
||||||
|
if [[ ${1:-} == "--inline" ]]; then
|
||||||
|
inline=(--inline)
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (($# == 0)); then
|
||||||
|
echo "Usage: omarchy agent prompt [--inline] <prompt...>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec omarchy-agent "${inline[@]}" --prompt "$*"
|
||||||
@@ -765,24 +765,67 @@ def probe_limits(access_token: str) -> dict[str, Any]:
|
|||||||
return {"ok": True, "limits": limits}
|
return {"ok": True, "limits": limits}
|
||||||
|
|
||||||
|
|
||||||
|
# A cached percentage outlives the probe that measured it, but only until its
|
||||||
|
# window rolls over: once a window has reset, the figure describes a period
|
||||||
|
# that is over, and a stale 78% would misreport an allowance that is now
|
||||||
|
# untouched. A window with no reset time, or one that will not parse, is kept
|
||||||
|
# — an unreadable timestamp is no reason to throw away a real number.
|
||||||
|
def limit_window_open(entry: dict[str, Any], now: dt.datetime) -> bool:
|
||||||
|
raw = str(entry.get("resetsAt") or "")
|
||||||
|
if raw == "":
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
resets_at = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
if resets_at.tzinfo is None:
|
||||||
|
resets_at = resets_at.replace(tzinfo=dt.timezone.utc)
|
||||||
|
return resets_at > now
|
||||||
|
|
||||||
|
|
||||||
|
def usable_cached_limits(cached: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
|
entries = cached.get("limits")
|
||||||
|
if not isinstance(entries, list):
|
||||||
|
return []
|
||||||
|
now = dt.datetime.now(dt.timezone.utc)
|
||||||
|
return [entry for entry in entries if isinstance(entry, dict) and limit_window_open(entry, now)]
|
||||||
|
|
||||||
|
|
||||||
def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[str, Any]:
|
def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[str, Any]:
|
||||||
result = {"limits": [], "usageStatusText": "", "authHelpText": AUTH_HELP}
|
result = {"limits": [], "usageStatusText": "", "authHelpText": AUTH_HELP}
|
||||||
|
|
||||||
if access_token == "":
|
|
||||||
result["usageStatusText"] = "Waiting for auth"
|
|
||||||
return result
|
|
||||||
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
|
|
||||||
return result
|
|
||||||
|
|
||||||
# A panel that is opened and shut repeatedly must not turn into a request
|
# A panel that is opened and shut repeatedly must not turn into a request
|
||||||
# per flick, so recent probe results are reused for a short window — and
|
# per flick, so recent probe results are reused for a short window — and
|
||||||
# kept as the answer of record when a later probe fails.
|
# kept as the answer of record when a later probe fails.
|
||||||
probe_cache = cache_root() / "claude-limits.json"
|
probe_cache = cache_root() / "claude-limits.json"
|
||||||
cached = read_fresh_json(probe_cache, float("inf")) or {}
|
cached = read_fresh_json(probe_cache, float("inf")) or {}
|
||||||
|
fallback = usable_cached_limits(cached)
|
||||||
|
|
||||||
|
# Probing needs a live token and only the Claude Code CLI can mint one: it
|
||||||
|
# refreshes the credential file when it runs, so a machine left alone long
|
||||||
|
# enough finds the saved token lapsed. Say so — an empty limits list with
|
||||||
|
# nothing else set hides the whole section and explains nothing — and keep
|
||||||
|
# showing the last numbers whose window has not since reset.
|
||||||
|
if access_token == "":
|
||||||
|
result["limits"] = fallback
|
||||||
|
result["usageStatusText"] = "Waiting for auth"
|
||||||
|
return result
|
||||||
|
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
|
||||||
|
result["limits"] = fallback
|
||||||
|
result["usageStatusText"] = "Sign-in expired"
|
||||||
|
result["authHelpText"] = (
|
||||||
|
"Claude Code's saved sign-in expired"
|
||||||
|
+ (" — showing the last known limits." if fallback else ".")
|
||||||
|
+ " Start Claude Code, or run `claude auth login`, to refresh it."
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# --force is a person asking for fresh numbers, so it skips the reuse window
|
||||||
|
# entirely; the interval is there to absorb repeated panel opens, not to
|
||||||
|
# overrule someone who pressed refresh.
|
||||||
fetched_at = number(cached.get("fetchedAtMs")) / 1000
|
fetched_at = number(cached.get("fetchedAtMs")) / 1000
|
||||||
min_interval = 0 if force else PROBE_MIN_INTERVAL_SECONDS
|
if fallback and not force and time.time() - fetched_at < PROBE_MIN_INTERVAL_SECONDS:
|
||||||
if cached.get("limits") and time.time() - fetched_at < max(min_interval, PROBE_MIN_INTERVAL_SECONDS):
|
result["limits"] = fallback
|
||||||
result["limits"] = cached["limits"]
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
probe = probe_limits(access_token)
|
probe = probe_limits(access_token)
|
||||||
@@ -795,8 +838,8 @@ def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[s
|
|||||||
# route. Ask the shell to try again sooner than its regular interval.
|
# route. Ask the shell to try again sooner than its regular interval.
|
||||||
if probe.get("transport"):
|
if probe.get("transport"):
|
||||||
result["retryAdvised"] = True
|
result["retryAdvised"] = True
|
||||||
if cached.get("limits"):
|
if fallback:
|
||||||
result["limits"] = cached["limits"]
|
result["limits"] = fallback
|
||||||
else:
|
else:
|
||||||
result["usageStatusText"] = "Claude limits unavailable"
|
result["usageStatusText"] = "Claude limits unavailable"
|
||||||
result["authHelpText"] = probe["helpText"]
|
result["authHelpText"] = probe["helpText"]
|
||||||
|
|||||||
+196
-24
@@ -11,12 +11,16 @@ panel only ever reads the JSON this prints.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import select
|
import select
|
||||||
import shutil
|
import shutil
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -25,6 +29,14 @@ AGENT_ID = "codex"
|
|||||||
AGENT_NAME = "Codex"
|
AGENT_NAME = "Codex"
|
||||||
AUTH_HELP = "Run `codex login` to authenticate."
|
AUTH_HELP = "Run `codex login` to authenticate."
|
||||||
|
|
||||||
|
# A scan this recent is only reused to dedup concurrent collector runs (the
|
||||||
|
# update command backgrounds one per agent while the panel refreshes on its
|
||||||
|
# own); every periodic widget refresh lands a real rescan, however low
|
||||||
|
# refreshIntervalSec is set. --limits-only promises only fresh limits, so it
|
||||||
|
# may reuse a scan for up to 15 minutes.
|
||||||
|
SCAN_REUSE_SECONDS = 20
|
||||||
|
LIMITS_ONLY_REUSE_SECONDS = 900
|
||||||
|
|
||||||
|
|
||||||
def local_day(value):
|
def local_day(value):
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -197,16 +209,47 @@ def scan_opencode_sessions():
|
|||||||
# A subscription burned entirely through opencode leaves no native session
|
# A subscription burned entirely through opencode leaves no native session
|
||||||
# files, but opencode records per-message provider, model, and token usage
|
# files, but opencode records per-message provider, model, and token usage
|
||||||
# in its own database. Read-only: opencode may be writing right now.
|
# in its own database. Read-only: opencode may be writing right now.
|
||||||
|
# Returns whether the scan ran to completion: a scan cut short by a
|
||||||
|
# database error still contributes what it read, but must not be cached
|
||||||
|
# as if it were the whole story.
|
||||||
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
|
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
|
||||||
if not db.is_file():
|
if not db.is_file():
|
||||||
return
|
return True
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(db.resolve().as_uri() + "?mode=ro", uri=True, timeout=2)
|
conn = sqlite3.connect(db.resolve().as_uri() + "?mode=ro", uri=True, timeout=2)
|
||||||
except sqlite3.Error:
|
except sqlite3.Error:
|
||||||
return
|
return False
|
||||||
try:
|
try:
|
||||||
conn.execute("PRAGMA query_only = ON")
|
conn.execute("PRAGMA query_only = ON")
|
||||||
for session_id, raw in conn.execute("SELECT session_id, data FROM message"):
|
# OpenCode DBs grow huge (every historical message, JSON included), and
|
||||||
|
# Python-side json.loads of every row wasted ~600 MB of RSS on machines
|
||||||
|
# whose subscription never ran on OpenAI. The json_extract conditions are
|
||||||
|
# the authority for the rows that reach them: role == "assistant" and
|
||||||
|
# providerID == "openai", the same exact-match values the Python filter
|
||||||
|
# below checks. The guards in front are pure acceleration, not a perfect
|
||||||
|
# proxy for the Python filter:
|
||||||
|
# - The LIKE gates skip rows whose JSON cannot contain the two
|
||||||
|
# key/value pairs, avoiding the JSON parse of tens-of-MB blobs. They
|
||||||
|
# can differ from json.loads on duplicate keys (Python keeps the
|
||||||
|
# last, SQLite json_extract keeps the first) and ASCII-escaped
|
||||||
|
# values (\"ass\\u0069stant\" decodes for Python but not for LIKE),
|
||||||
|
# so a row the authority would accept can be gated out. Both cases
|
||||||
|
# are vanishingly rare in real opencode data.
|
||||||
|
# - json_valid guards the parse itself: json_extract() RAISES on
|
||||||
|
# malformed JSON instead of returning NULL, and one such row would
|
||||||
|
# otherwise abort the whole scan. SQLite does not promise that AND
|
||||||
|
# terms evaluate left to right, so the guard is a CASE around each
|
||||||
|
# json_extract rather than a separate AND term. Rows that are not
|
||||||
|
# well-formed JSON are skipped here; the per-row try/except below
|
||||||
|
# stays as the final safety net for rows that pass the SQL filter
|
||||||
|
# but fail json.loads.
|
||||||
|
for session_id, raw in conn.execute(
|
||||||
|
"SELECT session_id, data FROM message"
|
||||||
|
" WHERE data LIKE '%\"role\"%:%\"assistant\"%'"
|
||||||
|
" AND data LIKE '%\"providerID\"%:%\"openai\"%'"
|
||||||
|
" AND CASE WHEN json_valid(data) THEN json_extract(data, '$.role') END = 'assistant'"
|
||||||
|
" AND CASE WHEN json_valid(data) THEN json_extract(data, '$.providerID') END = 'openai'"
|
||||||
|
):
|
||||||
# One malformed row must not abort the scan, so every shape assumption
|
# One malformed row must not abort the scan, so every shape assumption
|
||||||
# lives inside the try.
|
# lives inside the try.
|
||||||
try:
|
try:
|
||||||
@@ -232,9 +275,12 @@ def scan_opencode_sessions():
|
|||||||
continue
|
continue
|
||||||
add_usage(day, "opencode:" + str(session_id), model, input_tokens, output_tokens, cache_read, cache_write)
|
add_usage(day, "opencode:" + str(session_id), model, input_tokens, output_tokens, cache_read, cache_write)
|
||||||
except sqlite3.Error:
|
except sqlite3.Error:
|
||||||
pass
|
# Transient lock, schema migration, corruption: the numbers stop here,
|
||||||
|
# incomplete.
|
||||||
|
return False
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def scan_native_codex_sessions():
|
def scan_native_codex_sessions():
|
||||||
@@ -291,6 +337,143 @@ def scan_native_codex_sessions():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def cache_root():
|
||||||
|
root = Path(os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")) / "omarchy" / "agent-usage"
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def scan_cache_paths():
|
||||||
|
codex_home = Path(os.environ.get("CODEX_HOME") or (Path.home() / ".codex"))
|
||||||
|
db = Path(os.environ.get("XDG_DATA_HOME") or (Path.home() / ".local" / "share")) / "opencode" / "opencode.db"
|
||||||
|
# The digest covers every data path the scan reads: the codex session
|
||||||
|
# roots, the opencode DB, and (via Path.home()) the pi/omp session roots.
|
||||||
|
digest = hashlib.sha1((str(Path.home()) + "\n" + str(codex_home) + "\n" + str(db)).encode("utf-8")).hexdigest()[:16]
|
||||||
|
root = cache_root()
|
||||||
|
return root / f"codex-scan-{digest}.json", root / f"codex-scan-{digest}.lock"
|
||||||
|
|
||||||
|
|
||||||
|
def read_fresh_json(path, max_age_seconds):
|
||||||
|
if max_age_seconds <= 0 or not path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
# A negative age means the mtime is in the future: the clock moved
|
||||||
|
# backwards since the write, so the cache's freshness cannot be trusted.
|
||||||
|
age = time.time() - path.stat().st_mtime
|
||||||
|
if 0 <= age <= max_age_seconds:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path, payload):
|
||||||
|
# A temp name unique to this writer, not derived from the target: several
|
||||||
|
# collectors can run at once (the update command backgrounds one per agent,
|
||||||
|
# the panel refreshes on its own), and a shared temp path means the second
|
||||||
|
# replace finds the first one's file already moved away.
|
||||||
|
handle_fd, tmp_name = tempfile.mkstemp(dir=path.parent, prefix=path.name + ".", suffix=".tmp")
|
||||||
|
tmp = Path(tmp_name)
|
||||||
|
try:
|
||||||
|
with os.fdopen(handle_fd, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write(json.dumps(payload, separators=(",", ":")) + "\n")
|
||||||
|
# mkstemp opens at 0600; nothing in the cache is sensitive, so open it
|
||||||
|
# up to the usual 0644.
|
||||||
|
tmp.chmod(0o644)
|
||||||
|
tmp.replace(path)
|
||||||
|
except BaseException:
|
||||||
|
tmp.unlink(missing_ok=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# The cache payload is a versioned envelope around the local-stats dict, so a
|
||||||
|
# corrupted or foreign-shaped file is a cache miss (rescan + rewrite) instead
|
||||||
|
# of a crash or a garbage record.
|
||||||
|
def read_cached_stats(cache_file, max_age_seconds):
|
||||||
|
cached = read_fresh_json(cache_file, max_age_seconds)
|
||||||
|
if not isinstance(cached, dict) or cached.get("schemaVersion") != 1:
|
||||||
|
return None
|
||||||
|
# today* fields only mean "today" on the day they were scanned. A cache
|
||||||
|
# from another local date (midnight passed, or the clock moved) is a miss,
|
||||||
|
# not merely old, whatever its mtime says.
|
||||||
|
if cached.get("scanDate") != today:
|
||||||
|
return None
|
||||||
|
stats = cached.get("stats")
|
||||||
|
if not isinstance(stats, dict):
|
||||||
|
return None
|
||||||
|
if not all(key in stats for key in ("todayPrompts", "todayTotalTokens", "recentDays", "activeDates", "modelUsage")):
|
||||||
|
return None
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def write_cached_stats(cache_file, stats):
|
||||||
|
try:
|
||||||
|
write_json(cache_file, {"schemaVersion": 1, "scanDate": today, "stats": stats})
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"omarchy-agent-usage-codex: could not write usage cache ({exc})", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
def local_stats():
|
||||||
|
"""Snapshot the aggregated local usage into the record's stats dict."""
|
||||||
|
return {
|
||||||
|
"todayPrompts": today_prompts,
|
||||||
|
"todaySessions": len(today_sessions),
|
||||||
|
"todayTotalTokens": today_total_tokens,
|
||||||
|
"todayTokensByModel": today_tokens_by_model,
|
||||||
|
"recentDays": [recent[day] for day in recent_dates],
|
||||||
|
"totalPrompts": total_prompts,
|
||||||
|
"totalSessions": len(total_sessions),
|
||||||
|
# Days with any recorded usage, for the all-time "N days" summary. The
|
||||||
|
# dates travel too: merging snapshots from several machines needs their
|
||||||
|
# union, which a count alone cannot give.
|
||||||
|
"activeDays": len(active_days),
|
||||||
|
"activeDates": sorted(active_days),
|
||||||
|
"modelUsage": model_usage,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_local_scans():
|
||||||
|
scan_pi_sessions()
|
||||||
|
scan_native_codex_sessions()
|
||||||
|
complete = scan_opencode_sessions()
|
||||||
|
return local_stats(), complete
|
||||||
|
|
||||||
|
|
||||||
|
def cached_local_stats(max_age):
|
||||||
|
"""Local stats, with the cache as a pure optimization.
|
||||||
|
|
||||||
|
The cache must never take the collector down: any cache-layer failure
|
||||||
|
(unwritable cache root, lock errors, disk full) degrades to a direct scan
|
||||||
|
and a warning on stderr. The JSON record is the contract; the cache is not.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return _cached_local_stats(max_age)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"omarchy-agent-usage-codex: cache unavailable ({exc}); scanning directly", file=sys.stderr)
|
||||||
|
stats, _ = run_local_scans()
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def _cached_local_stats(max_age):
|
||||||
|
cache_file, lock_file = scan_cache_paths()
|
||||||
|
|
||||||
|
cached = read_cached_stats(cache_file, max_age)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
with lock_file.open("w") as lock:
|
||||||
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
||||||
|
cached = read_cached_stats(cache_file, max_age)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
stats, complete = run_local_scans()
|
||||||
|
# An interrupted scan still serves this run, but caching it would
|
||||||
|
# suppress the missing usage for every reader until the cache expires.
|
||||||
|
if complete:
|
||||||
|
write_cached_stats(cache_file, stats)
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
def rpc_request(proc, request_id, method, params=None, timeout=8):
|
def rpc_request(proc, request_id, method, params=None, timeout=8):
|
||||||
payload = {"id": request_id, "method": method, "params": params or {}}
|
payload = {"id": request_id, "method": method, "params": params or {}}
|
||||||
proc.stdin.write(json.dumps(payload) + "\n")
|
proc.stdin.write(json.dumps(payload) + "\n")
|
||||||
@@ -390,16 +573,17 @@ def fetch_codex_rpc():
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
# Local stats and limits come from the same cheap scan, so there is no
|
# --force rescans everything and rewrites the cache. --limits-only is kept
|
||||||
# cache to force past and no faster limits-only path. The flags exist so
|
# for CLI compatibility with the panel's refreshLimits() call: only the
|
||||||
# every collector accepts the same invocation.
|
# limits probe must be fresh, so it may reuse a scan for far longer than a
|
||||||
|
# normal run, whose short window exists purely to dedup concurrent
|
||||||
|
# collector runs.
|
||||||
parser.add_argument("--force", action="store_true")
|
parser.add_argument("--force", action="store_true")
|
||||||
parser.add_argument("--limits-only", action="store_true")
|
parser.add_argument("--limits-only", action="store_true")
|
||||||
parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
scan_pi_sessions()
|
max_age = 0 if args.force else (LIMITS_ONLY_REUSE_SECONDS if args.limits_only else SCAN_REUSE_SECONDS)
|
||||||
scan_native_codex_sessions()
|
stats = cached_local_stats(max_age)
|
||||||
scan_opencode_sessions()
|
|
||||||
rpc = fetch_codex_rpc()
|
rpc = fetch_codex_rpc()
|
||||||
|
|
||||||
record = {
|
record = {
|
||||||
@@ -409,20 +593,8 @@ def main():
|
|||||||
"updatedAt": datetime.now(timezone.utc).isoformat(),
|
"updatedAt": datetime.now(timezone.utc).isoformat(),
|
||||||
"ready": True,
|
"ready": True,
|
||||||
"hasLocalStats": True,
|
"hasLocalStats": True,
|
||||||
"todayPrompts": today_prompts,
|
|
||||||
"todaySessions": len(today_sessions),
|
|
||||||
"todayTotalTokens": today_total_tokens,
|
|
||||||
"todayTokensByModel": today_tokens_by_model,
|
|
||||||
"recentDays": [recent[day] for day in recent_dates],
|
|
||||||
"totalPrompts": total_prompts,
|
|
||||||
"totalSessions": len(total_sessions),
|
|
||||||
# Days with any recorded usage, for the all-time "N days" summary. The
|
|
||||||
# dates travel too: merging snapshots from several machines needs their
|
|
||||||
# union, which a count alone cannot give.
|
|
||||||
"activeDays": len(active_days),
|
|
||||||
"activeDates": sorted(active_days),
|
|
||||||
"modelUsage": model_usage,
|
|
||||||
}
|
}
|
||||||
|
record.update(stats)
|
||||||
record.update(rpc)
|
record.update(rpc)
|
||||||
print(json.dumps(record, separators=(",", ":")))
|
print(json.dumps(record, separators=(",", ":")))
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ start_webcam_overlay() {
|
|||||||
|
|
||||||
# Auto-detect first available webcam if none specified
|
# Auto-detect first available webcam if none specified
|
||||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||||
WEBCAM_DEVICE=$(v4l2-ctl --list-devices 2>/dev/null | grep -m1 "^[[:space:]]*/dev/video" | tr -d '\t')
|
WEBCAM_DEVICE=$(omarchy-capture-webcam-list | sed -n '1s/[[:space:]].*//p')
|
||||||
if [[ -z $WEBCAM_DEVICE ]]; then
|
if [[ -z $WEBCAM_DEVICE ]]; then
|
||||||
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
|
omarchy-notification-send -u critical -t 3000 "No webcam devices found"
|
||||||
return 1
|
return 1
|
||||||
@@ -228,7 +228,7 @@ stop_screenrecording() {
|
|||||||
|
|
||||||
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \
|
omarchy-notification-send "Screen recording saved" "Open with Super + Alt + , (or click this)" \
|
||||||
-t 10000 --image "${preview:-$filename}" \
|
-t 10000 --image "${preview:-$filename}" \
|
||||||
--exec "$(printf 'mpv %q' "$filename")"
|
--exec mpv -- "$filename"
|
||||||
|
|
||||||
# The shell loads the thumbnail into memory when the toast appears and never
|
# The shell loads the thumbnail into memory when the toast appears and never
|
||||||
# re-reads the file, so the preview only has to outlive that load -- not the
|
# re-reads the file, so the preview only has to outlive that load -- not the
|
||||||
|
|||||||
@@ -3,21 +3,7 @@
|
|||||||
# omarchy:summary=Pick a webcam and start a screen recording with it
|
# omarchy:summary=Pick a webcam and start a screen recording with it
|
||||||
# omarchy:examples=omarchy capture screenrecording-with-webcam
|
# omarchy:examples=omarchy capture screenrecording-with-webcam
|
||||||
|
|
||||||
set -o pipefail
|
mapfile -t devices < <(omarchy-capture-webcam-list)
|
||||||
|
|
||||||
webcam_list() {
|
|
||||||
v4l2-ctl --list-devices 2>/dev/null | while IFS= read -r line; do
|
|
||||||
if [[ $line != $'\t'* && -n $line ]]; then
|
|
||||||
local name="$line"
|
|
||||||
local device
|
|
||||||
IFS= read -r device || break
|
|
||||||
device=$(tr -d '\t' <<<"$device" | head -1)
|
|
||||||
[[ -n $device ]] && echo "$device $name"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
mapfile -t devices < <(webcam_list)
|
|
||||||
if (( ${#devices[@]} == 0 )); then
|
if (( ${#devices[@]} == 0 )); then
|
||||||
omarchy-notification-send "No webcam devices found" -u critical -t 3000
|
omarchy-notification-send "No webcam devices found" -u critical -t 3000
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ case "$PROCESSING" in
|
|||||||
# notification outage must not report the capture itself as failed.
|
# notification outage must not report the capture itself as failed.
|
||||||
omarchy-notification-send "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" \
|
omarchy-notification-send "Screenshot saved to clipboard and file" "Edit with Super + Alt + , (or click this)" \
|
||||||
--image "$FILEPATH" \
|
--image "$FILEPATH" \
|
||||||
--exec "$(printf '%q %q' "$SCREENSHOT_EDITOR" "$FILEPATH")" || true
|
--exec "$SCREENSHOT_EDITOR" "$FILEPATH" || true
|
||||||
;;
|
;;
|
||||||
copy)
|
copy)
|
||||||
grim -g "$SELECTION" - | wl-copy --type image/png
|
grim -g "$SELECTION" - | wl-copy --type image/png
|
||||||
|
|||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=List webcam devices that support video capture
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
capture_capable() {
|
||||||
|
local device="$1"
|
||||||
|
|
||||||
|
v4l2-ctl --device "$device" --info 2>/dev/null | awk '
|
||||||
|
/^[[:space:]]*Device Caps[[:space:]]*:/ { inspect = 1; next }
|
||||||
|
inspect && /^[[:space:]]*Video Capture/ { found = 1 }
|
||||||
|
END { exit !found }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
name=""
|
||||||
|
emitted=0
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ -n $line && $line != [[:space:]]* ]]; then
|
||||||
|
name="$line"
|
||||||
|
emitted=0
|
||||||
|
elif (( ! emitted )); then
|
||||||
|
device="${line#"${line%%[![:space:]]*}"}"
|
||||||
|
|
||||||
|
if [[ $device == /dev/video* ]] && capture_capable "$device"; then
|
||||||
|
emitted=1
|
||||||
|
printf '%s %s\n' "$device" "$name"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(v4l2-ctl --list-devices 2>/dev/null)
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -25,6 +25,51 @@ valid_url() {
|
|||||||
[[ $1 =~ ^https?:// ]]
|
[[ $1 =~ ^https?:// ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# A printed path is only usable if it is a regular file inside DOWNLOAD_DIR.
|
||||||
|
# Forged records (leading-dash mpv options, paths with control chars, or
|
||||||
|
# anything that escaped the download directory) must not reach the click command.
|
||||||
|
resolve_download_file() {
|
||||||
|
local candidate=$1 file_real dir_real
|
||||||
|
|
||||||
|
[[ -n $candidate ]] || return 1
|
||||||
|
[[ $candidate != *$'\n'* && $candidate != *$'\r'* && $candidate != *$'\t'* ]] || return 1
|
||||||
|
[[ -f $candidate ]] || return 1
|
||||||
|
|
||||||
|
# Read to a NUL: command substitution strips trailing newlines, which would
|
||||||
|
# resolve a name ending in one to a different file that may well exist.
|
||||||
|
IFS= read -r -d '' file_real < <(realpath -ze -- "$candidate") || return 1
|
||||||
|
IFS= read -r -d '' dir_real < <(realpath -ze -- "$DOWNLOAD_DIR") || return 1
|
||||||
|
|
||||||
|
[[ $file_real != *$'\n'* && $file_real != *$'\r'* && $file_real != *$'\t'* ]] || return 1
|
||||||
|
# Trim the slash so a download directory of "/" still leaves a usable prefix.
|
||||||
|
[[ $file_real == "${dir_real%/}"/* ]] || return 1
|
||||||
|
|
||||||
|
printf '%s' "$file_real"
|
||||||
|
}
|
||||||
|
|
||||||
|
# yt-dlp prints the title JSON-encoded, so a newline or tab in page metadata is an
|
||||||
|
# escape sequence rather than a record boundary. This is toast text, never a command.
|
||||||
|
decode_title() {
|
||||||
|
local decoded
|
||||||
|
|
||||||
|
decoded=$(jq -r 'if type == "string" then . else empty end' <<<"$1" 2>/dev/null) || return 1
|
||||||
|
decoded=${decoded%%[[:cntrl:]]*} # keep what a person would read, drop the forgery
|
||||||
|
[[ -n $decoded && $decoded != -* ]] || return 1
|
||||||
|
|
||||||
|
printf '%s' "$decoded"
|
||||||
|
}
|
||||||
|
|
||||||
|
title_from_file() {
|
||||||
|
local name=${1##*/}
|
||||||
|
name=${name%.*}
|
||||||
|
name=${name//[$'\n\r\t']/}
|
||||||
|
if [[ -z $name || $name == -* ]]; then
|
||||||
|
printf '%s' "Video"
|
||||||
|
else
|
||||||
|
printf '%s' "$name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Drive the Quickshell OSD — a single overlay that updates in place (like the
|
# Drive the Quickshell OSD — a single overlay that updates in place (like the
|
||||||
# volume/brightness bar), so download progress never stacks like notifications.
|
# volume/brightness bar), so download progress never stacks like notifications.
|
||||||
osd_progress() {
|
osd_progress() {
|
||||||
@@ -41,16 +86,19 @@ download_url() {
|
|||||||
mkdir -p "$DOWNLOAD_DIR"
|
mkdir -p "$DOWNLOAD_DIR"
|
||||||
|
|
||||||
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
# Don't show anything until yt-dlp confirms there's actually a video to grab.
|
||||||
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings "$url" >/dev/null 2>&1; then
|
if ! yt-dlp --no-playlist --simulate --quiet --no-warnings --no-exec --no-exec-before-download -- "$url" >/dev/null 2>&1; then
|
||||||
omarchy-notification-send -u critical -g "No video found for download" "$url"
|
omarchy-notification-send -u critical -g "No video found for download" "$url"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
osd_progress 0
|
osd_progress 0
|
||||||
|
|
||||||
# Stream the download: OMARCHY_PROG carries the percent (drives the OSD),
|
# Stream the download: OMARCHY_PROG carries the percent (drives the OSD), and
|
||||||
# OMARCHY_FILE (printed only after a successful move) carries title + path.
|
# OMARCHY_FILE and OMARCHY_TITLE (printed only after a successful move) carry the
|
||||||
local line rest pct intpct last="" er nowms lastms=0 title="" filepath=""
|
# path and the title. The title is JSON-encoded so metadata cannot forge a record,
|
||||||
|
# and the file is named after it: yt-dlp strips control characters from a filename
|
||||||
|
# with or without --restrict-filenames, so a record is still only ever one line.
|
||||||
|
local line pct intpct last="" er nowms lastms=0 title="" filepath="" resolved
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
case $line in
|
case $line in
|
||||||
OMARCHY_PROG*)
|
OMARCHY_PROG*)
|
||||||
@@ -67,22 +115,26 @@ download_url() {
|
|||||||
osd_progress "$intpct"
|
osd_progress "$intpct"
|
||||||
;;
|
;;
|
||||||
OMARCHY_FILE*)
|
OMARCHY_FILE*)
|
||||||
rest=${line#OMARCHY_FILE$'\t'}
|
resolved=$(resolve_download_file "${line#OMARCHY_FILE$'\t'}") || continue
|
||||||
title=${rest%%$'\t'*}
|
filepath=$resolved
|
||||||
filepath=${rest#*$'\t'}
|
;;
|
||||||
|
OMARCHY_TITLE*)
|
||||||
|
title=$(decode_title "${line#OMARCHY_TITLE$'\t'}") || title=""
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done < <(PYTHONUNBUFFERED=1 yt-dlp --no-playlist --restrict-filenames --no-simulate \
|
done < <(PYTHONUNBUFFERED=1 yt-dlp --no-playlist --no-simulate \
|
||||||
--quiet --no-warnings --progress --newline \
|
--quiet --no-warnings --no-exec --no-exec-before-download --progress --newline \
|
||||||
--progress-template $'download:OMARCHY_PROG\t%(progress._percent_str)s' \
|
--progress-template $'download:OMARCHY_PROG\t%(progress._percent_str)s' \
|
||||||
--paths "$DOWNLOAD_DIR" -o '%(title)s [%(id)s].%(ext)s' \
|
--paths "$DOWNLOAD_DIR" -o '%(title)s.%(ext)s' \
|
||||||
--print $'after_move:OMARCHY_FILE\t%(title)s\t%(filepath)s' \
|
--print $'after_move:OMARCHY_FILE\t%(filepath)s' \
|
||||||
"$url" 2>&1)
|
--print $'after_move:OMARCHY_TITLE\t%(title)j' \
|
||||||
|
-- "$url" 2>&1)
|
||||||
|
|
||||||
osd_close
|
osd_close
|
||||||
|
|
||||||
# after_move only prints on a successful download+move, so a captured path == success.
|
# after_move only prints on a successful download+move, so a captured path == success.
|
||||||
if [[ -n $filepath ]]; then
|
if [[ -n $filepath ]]; then
|
||||||
|
[[ -n $title ]] || title=$(title_from_file "$filepath")
|
||||||
((${#title} > 50)) && title="${title:0:50}…" # keep the toast compact
|
((${#title} > 50)) && title="${title:0:50}…" # keep the toast compact
|
||||||
|
|
||||||
# Square, center-cropped thumbnail so the notification preview isn't stretched.
|
# Square, center-cropped thumbnail so the notification preview isn't stretched.
|
||||||
@@ -94,9 +146,11 @@ download_url() {
|
|||||||
|
|
||||||
# Best-effort: the download already succeeded, and under `set -e` a failed
|
# Best-effort: the download already succeeded, and under `set -e` a failed
|
||||||
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
# toast would exit before the thumbnail cleanup below is ever scheduled.
|
||||||
|
# `--` keeps mpv from parsing a leading-dash filename as an option; the path
|
||||||
|
# is one discrete argument, so it never reaches a shell.
|
||||||
omarchy-notification-send -g "Download complete" "$title" \
|
omarchy-notification-send -g "Download complete" "$title" \
|
||||||
-t 10000 --image "${preview:-$filepath}" \
|
-t 10000 --image "${preview:-$filepath}" \
|
||||||
--exec "$(printf 'mpv %q' "$filepath")" || true
|
--exec mpv -- "$filepath" || true
|
||||||
|
|
||||||
# The shell loads the thumbnail into memory when the toast appears and never
|
# The shell loads the thumbnail into memory when the toast appears and never
|
||||||
# re-reads the file, so the preview only has to outlive that load, not the
|
# re-reads the file, so the preview only has to outlive that load, not the
|
||||||
|
|||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Watch for process crashes and offer an AI diagnosis
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
# systemd-coredump journals every core dump under a known MESSAGE_ID with
|
||||||
|
# structured COREDUMP_* fields, which carry more than the core filenames do.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# See systemd.journal-fields(7).
|
||||||
|
readonly COREDUMP_MESSAGE_ID=fc2e22bc6ee647b6b90729ab34a250b1
|
||||||
|
|
||||||
|
# nf-md-robot_dead, escaped so this file reads without a Nerd Font.
|
||||||
|
readonly CRASH_GLYPH=$'\U000f16a1'
|
||||||
|
|
||||||
|
# Crash loops dump core repeatedly, so announce each program at most once a
|
||||||
|
# window.
|
||||||
|
readonly dedupe_seconds=${OMARCHY_CRASH_DEDUPE_SECONDS:-60}
|
||||||
|
|
||||||
|
# Extended regex of process names never worth announcing.
|
||||||
|
readonly ignore_pattern=${OMARCHY_CRASH_IGNORE:-}
|
||||||
|
|
||||||
|
declare -A last_notified
|
||||||
|
|
||||||
|
announce() {
|
||||||
|
local comm=$1 pid=$2 exe=$3 signal=$4
|
||||||
|
|
||||||
|
# The shell owns org.freedesktop.Notifications, so a shell crash takes the
|
||||||
|
# notification server down with it and a toast sent into that gap is lost.
|
||||||
|
# Wait for the restarted shell to claim the name again: the crash least
|
||||||
|
# likely to be delivered is the one most worth reporting.
|
||||||
|
omarchy-notification-wait || return 1
|
||||||
|
|
||||||
|
# --exec rather than a libnotify action: the shell runs clicks from its own
|
||||||
|
# hint and never emits ActionInvoked. Keeps the default "omarchy-action" app
|
||||||
|
# name, the only one shouldBypassDnd() lets through. Crash details ride as
|
||||||
|
# discrete argv words, so a hostile process name can't be reparsed as a
|
||||||
|
# command. --exec consumes the rest of the line, so it comes last.
|
||||||
|
omarchy-notification-send \
|
||||||
|
--urgency critical \
|
||||||
|
--glyph "$CRASH_GLYPH" \
|
||||||
|
"Process crashed: $comm" \
|
||||||
|
"Click to diagnose with AI" \
|
||||||
|
--exec omarchy-agent-crash "$pid" "$comm" "$exe" "$signal"
|
||||||
|
}
|
||||||
|
|
||||||
|
# -n 0 so a restart does not re-announce crashes already dealt with.
|
||||||
|
journalctl -f -n 0 -o json "MESSAGE_ID=$COREDUMP_MESSAGE_ID" 2>/dev/null |
|
||||||
|
while IFS= read -r entry; do
|
||||||
|
IFS=$'\t' read -r uid comm pid exe signal < <(
|
||||||
|
jq -r '[(._UID // "-"),
|
||||||
|
(.COREDUMP_COMM // "-"),
|
||||||
|
(.COREDUMP_PID // "-"),
|
||||||
|
(.COREDUMP_EXE // "-"),
|
||||||
|
(.COREDUMP_SIGNAL_NAME // "-")] | @tsv' <<<"$entry" 2>/dev/null
|
||||||
|
)
|
||||||
|
|
||||||
|
[[ $pid =~ ^[0-9]+$ ]] || continue
|
||||||
|
|
||||||
|
# The toast only offers a diagnosis, so it has nothing to offer until an
|
||||||
|
# agent is chosen. Checked per crash, not at startup, so picking one takes
|
||||||
|
# effect without restarting this service.
|
||||||
|
[[ -n $(omarchy-default-agent) ]] || continue
|
||||||
|
|
||||||
|
# Only this user's crashes; a daemon dumping core is a sysadmin's problem.
|
||||||
|
[[ $uid =~ ^[0-9]+$ ]] || continue
|
||||||
|
((uid == UID)) || continue
|
||||||
|
|
||||||
|
# comm is truncated to 15 characters, so prefer the executable's basename.
|
||||||
|
name=$comm
|
||||||
|
[[ $exe == /* ]] && name=${exe##*/}
|
||||||
|
|
||||||
|
[[ -n $ignore_pattern && $name =~ $ignore_pattern ]] && continue
|
||||||
|
|
||||||
|
# Never announce our own machinery, or it notifies about itself.
|
||||||
|
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
|
||||||
|
|
||||||
|
now=$EPOCHSECONDS
|
||||||
|
(((now - ${last_notified[$name]:-0}) < dedupe_seconds)) && continue
|
||||||
|
|
||||||
|
# Only a delivered toast starts the dedupe window. A failed send that
|
||||||
|
# counted would suppress the rest of a crash loop for a minute, and
|
||||||
|
# `journalctl -n 0` never replays what was missed.
|
||||||
|
announce "$name" "$pid" "$exe" "$signal" && last_notified[$name]=$now
|
||||||
|
done
|
||||||
@@ -17,7 +17,9 @@ if (($# == 0)); then
|
|||||||
read -r agent <"$agent_file"
|
read -r agent <"$agent_file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ -n $agent ]] && echo "$agent" || echo "opencode"
|
# Silent when unset rather than defaulting: Omarchy picks no agent for you, so
|
||||||
|
# the menu leaves every entry unchecked until one is chosen.
|
||||||
|
[[ -n ${agent:-} ]] && echo "$agent"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ printf '%s\n' "$agent" >"$agent_file"
|
|||||||
|
|
||||||
if [[ $installing == "true" ]]; then
|
if [[ $installing == "true" ]]; then
|
||||||
printf '\033[2J\033[3J\033[H'
|
printf '\033[2J\033[3J\033[H'
|
||||||
exec omarchy-launch-agent --inline
|
exec omarchy-agent --inline
|
||||||
else
|
else
|
||||||
exec omarchy-launch-agent
|
exec omarchy-agent
|
||||||
fi
|
fi
|
||||||
Executable
+652
@@ -0,0 +1,652 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# omarchy:summary=Add branded glyphs to the Omarchy icon font
|
||||||
|
# omarchy:args=[list|add] <name> <svg-file-or-url> [--codepoint U+E9xx] [--font PATH]
|
||||||
|
# omarchy:examples=omarchy dev font list | omarchy dev font add ollama https://simpleicons.org/icons/ollama.svg
|
||||||
|
"""Append monochrome SVG marks to default/fonts/omarchy/omarchy.ttf.
|
||||||
|
|
||||||
|
The font is a private-use icon font: the menu renders a mark by setting
|
||||||
|
"iconFont":"omarchy" on an entry and using the glyph's codepoint as its
|
||||||
|
icon. Adding a mark means appending a glyph here, then pointing a menu
|
||||||
|
entry at the codepoint this prints.
|
||||||
|
|
||||||
|
Marks must be monochrome single-path SVGs so the menu can draw them in the
|
||||||
|
active theme's foreground and selection colors. Brand icon sets like
|
||||||
|
simpleicons.org publish exactly that shape; app favicons often do not.
|
||||||
|
|
||||||
|
The glyph is scaled into the same 64..960 box the existing marks use, so a
|
||||||
|
new mark lands at the same optical size as the ones already in the font.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
UPEM = 1024
|
||||||
|
ART_BOX = (64, 64, 960, 960) # the box every existing mark is drawn in
|
||||||
|
TOL = 0.6 # cubic -> quadratic error tolerance, in font units
|
||||||
|
PUA_FIRST = 0xE900
|
||||||
|
|
||||||
|
|
||||||
|
# --- SVG path parsing ------------------------------------------------------
|
||||||
|
|
||||||
|
NUM = re.compile(r'[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?')
|
||||||
|
CMD = re.compile(r'[MmZzLlHhVvCcSsQqTtAa]')
|
||||||
|
|
||||||
|
|
||||||
|
def tokenize(d):
|
||||||
|
out, i = [], 0
|
||||||
|
while i < len(d):
|
||||||
|
c = d[i]
|
||||||
|
if CMD.match(c):
|
||||||
|
out.append(c)
|
||||||
|
i += 1
|
||||||
|
elif c in ' ,\t\r\n':
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
m = NUM.match(d, i)
|
||||||
|
if not m:
|
||||||
|
raise ValueError('bad path data at %d: %r' % (i, d[i:i + 20]))
|
||||||
|
out.append(float(m.group()))
|
||||||
|
i = m.end()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def arc_to_cubics(p0, rx, ry, phi, large, sweep, p1):
|
||||||
|
"""Endpoint-parameterized SVG arc -> cubic segments."""
|
||||||
|
if p0 == p1:
|
||||||
|
return []
|
||||||
|
rx, ry = abs(rx), abs(ry)
|
||||||
|
if rx == 0 or ry == 0:
|
||||||
|
return [('L', p1)]
|
||||||
|
phi = math.radians(phi % 360)
|
||||||
|
cosp, sinp = math.cos(phi), math.sin(phi)
|
||||||
|
dx2, dy2 = (p0[0] - p1[0]) / 2.0, (p0[1] - p1[1]) / 2.0
|
||||||
|
x1 = cosp * dx2 + sinp * dy2
|
||||||
|
y1 = -sinp * dx2 + cosp * dy2
|
||||||
|
lam = x1 * x1 / (rx * rx) + y1 * y1 / (ry * ry)
|
||||||
|
if lam > 1:
|
||||||
|
s = math.sqrt(lam)
|
||||||
|
rx, ry = rx * s, ry * s
|
||||||
|
num = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1
|
||||||
|
den = rx * rx * y1 * y1 + ry * ry * x1 * x1
|
||||||
|
co = math.sqrt(max(0.0, num / den)) if den else 0.0
|
||||||
|
if large == sweep:
|
||||||
|
co = -co
|
||||||
|
cx1 = co * rx * y1 / ry
|
||||||
|
cy1 = -co * ry * x1 / rx
|
||||||
|
cx = cosp * cx1 - sinp * cy1 + (p0[0] + p1[0]) / 2.0
|
||||||
|
cy = sinp * cx1 + cosp * cy1 + (p0[1] + p1[1]) / 2.0
|
||||||
|
|
||||||
|
def angle(ux, uy, vx, vy):
|
||||||
|
dot = ux * vx + uy * vy
|
||||||
|
n = math.hypot(ux, uy) * math.hypot(vx, vy)
|
||||||
|
a = math.acos(max(-1.0, min(1.0, dot / n))) if n else 0.0
|
||||||
|
return -a if ux * vy - uy * vx < 0 else a
|
||||||
|
|
||||||
|
theta = angle(1, 0, (x1 - cx1) / rx, (y1 - cy1) / ry)
|
||||||
|
delta = angle((x1 - cx1) / rx, (y1 - cy1) / ry, (-x1 - cx1) / rx, (-y1 - cy1) / ry)
|
||||||
|
if not sweep and delta > 0:
|
||||||
|
delta -= 2 * math.pi
|
||||||
|
elif sweep and delta < 0:
|
||||||
|
delta += 2 * math.pi
|
||||||
|
|
||||||
|
segs = []
|
||||||
|
n = max(1, int(math.ceil(abs(delta) / (math.pi / 2) - 1e-9)))
|
||||||
|
step = delta / n
|
||||||
|
k = 4.0 / 3.0 * math.tan(step / 4.0)
|
||||||
|
|
||||||
|
def at(t):
|
||||||
|
x, y = rx * math.cos(t), ry * math.sin(t)
|
||||||
|
return (cosp * x - sinp * y + cx, sinp * x + cosp * y + cy)
|
||||||
|
|
||||||
|
def deriv(t):
|
||||||
|
x, y = -rx * math.sin(t), ry * math.cos(t)
|
||||||
|
return (cosp * x - sinp * y, sinp * x + cosp * y)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
t0 = theta + i * step
|
||||||
|
t1 = t0 + step
|
||||||
|
a, b = at(t0), at(t1)
|
||||||
|
da, db = deriv(t0), deriv(t1)
|
||||||
|
segs.append(('C',
|
||||||
|
(a[0] + k * da[0], a[1] + k * da[1]),
|
||||||
|
(b[0] - k * db[0], b[1] - k * db[1]),
|
||||||
|
b))
|
||||||
|
return segs
|
||||||
|
|
||||||
|
|
||||||
|
def parse_path(d):
|
||||||
|
"""Return one list of absolute segments per subpath."""
|
||||||
|
t = tokenize(d)
|
||||||
|
subs, cur = [], None
|
||||||
|
pos = start = (0.0, 0.0)
|
||||||
|
prev_c = prev_q = None
|
||||||
|
i, cmd = 0, None
|
||||||
|
while i < len(t):
|
||||||
|
if isinstance(t[i], str):
|
||||||
|
cmd = t[i]
|
||||||
|
i += 1
|
||||||
|
rel = cmd.islower()
|
||||||
|
c = cmd.upper()
|
||||||
|
|
||||||
|
def take(n):
|
||||||
|
nonlocal i
|
||||||
|
v = t[i:i + n]
|
||||||
|
i += n
|
||||||
|
return v
|
||||||
|
|
||||||
|
def abspt(x, y):
|
||||||
|
return (pos[0] + x, pos[1] + y) if rel else (x, y)
|
||||||
|
|
||||||
|
if c == 'M':
|
||||||
|
x, y = take(2)
|
||||||
|
pos = start = abspt(x, y)
|
||||||
|
if cur:
|
||||||
|
subs.append(cur)
|
||||||
|
cur = [('M', pos)]
|
||||||
|
cmd = 'l' if rel else 'L' # implicit lineto for extra pairs
|
||||||
|
prev_c = prev_q = None
|
||||||
|
elif c == 'Z':
|
||||||
|
if cur:
|
||||||
|
subs.append(cur)
|
||||||
|
cur = None
|
||||||
|
pos = start
|
||||||
|
prev_c = prev_q = None
|
||||||
|
elif c in 'LHV':
|
||||||
|
if c == 'L':
|
||||||
|
x, y = take(2)
|
||||||
|
p = abspt(x, y)
|
||||||
|
elif c == 'H':
|
||||||
|
x = take(1)[0]
|
||||||
|
p = (pos[0] + x, pos[1]) if rel else (x, pos[1])
|
||||||
|
else:
|
||||||
|
y = take(1)[0]
|
||||||
|
p = (pos[0], pos[1] + y) if rel else (pos[0], y)
|
||||||
|
cur.append(('L', p))
|
||||||
|
pos = p
|
||||||
|
prev_c = prev_q = None
|
||||||
|
elif c in 'CS':
|
||||||
|
if c == 'C':
|
||||||
|
x1, y1, x2, y2, x, y = take(6)
|
||||||
|
c1, c2, p = abspt(x1, y1), abspt(x2, y2), abspt(x, y)
|
||||||
|
else:
|
||||||
|
x2, y2, x, y = take(4)
|
||||||
|
c2, p = abspt(x2, y2), abspt(x, y)
|
||||||
|
c1 = (2 * pos[0] - prev_c[0], 2 * pos[1] - prev_c[1]) if prev_c else pos
|
||||||
|
cur.append(('C', c1, c2, p))
|
||||||
|
prev_c, prev_q = c2, None
|
||||||
|
pos = p
|
||||||
|
elif c in 'QT':
|
||||||
|
if c == 'Q':
|
||||||
|
x1, y1, x, y = take(4)
|
||||||
|
q, p = abspt(x1, y1), abspt(x, y)
|
||||||
|
else:
|
||||||
|
x, y = take(2)
|
||||||
|
p = abspt(x, y)
|
||||||
|
q = (2 * pos[0] - prev_q[0], 2 * pos[1] - prev_q[1]) if prev_q else pos
|
||||||
|
cur.append(('C',
|
||||||
|
(pos[0] + 2.0 / 3 * (q[0] - pos[0]), pos[1] + 2.0 / 3 * (q[1] - pos[1])),
|
||||||
|
(p[0] + 2.0 / 3 * (q[0] - p[0]), p[1] + 2.0 / 3 * (q[1] - p[1])),
|
||||||
|
p))
|
||||||
|
prev_q, prev_c = q, None
|
||||||
|
pos = p
|
||||||
|
elif c == 'A':
|
||||||
|
rx, ry, rot, large, sweep, x, y = take(7)
|
||||||
|
p = abspt(x, y)
|
||||||
|
cur.extend(arc_to_cubics(pos, rx, ry, rot, int(large), int(sweep), p))
|
||||||
|
pos = p
|
||||||
|
prev_c = prev_q = None
|
||||||
|
else:
|
||||||
|
raise ValueError('unsupported path command %r' % cmd)
|
||||||
|
if cur:
|
||||||
|
subs.append(cur)
|
||||||
|
return subs
|
||||||
|
|
||||||
|
|
||||||
|
# --- outline conversion ----------------------------------------------------
|
||||||
|
|
||||||
|
def cubic_to_quads(p0, p1, p2, p3, tol, depth=0):
|
||||||
|
"""Approximate one cubic with quadratics: [(control, end), ...]."""
|
||||||
|
ex = p0[0] - 3 * p1[0] + 3 * p2[0] - p3[0]
|
||||||
|
ey = p0[1] - 3 * p1[1] + 3 * p2[1] - p3[1]
|
||||||
|
if math.sqrt(3) / 36 * math.hypot(ex, ey) <= tol or depth >= 8:
|
||||||
|
return [(((3 * p1[0] - p0[0] + 3 * p2[0] - p3[0]) / 4.0,
|
||||||
|
(3 * p1[1] - p0[1] + 3 * p2[1] - p3[1]) / 4.0), p3)]
|
||||||
|
|
||||||
|
def mid(a, b):
|
||||||
|
return ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0)
|
||||||
|
|
||||||
|
p01, p12, p23 = mid(p0, p1), mid(p1, p2), mid(p2, p3)
|
||||||
|
p012, p123 = mid(p01, p12), mid(p12, p23)
|
||||||
|
m = mid(p012, p123)
|
||||||
|
return (cubic_to_quads(p0, p01, p012, m, tol, depth + 1) +
|
||||||
|
cubic_to_quads(m, p123, p23, p3, tol, depth + 1))
|
||||||
|
|
||||||
|
|
||||||
|
def contours_from_svg(path_d, view):
|
||||||
|
"""SVG path -> TrueType contours [[(x, y, on_curve), ...], ...]."""
|
||||||
|
vbx, vby, vbw, vbh = view
|
||||||
|
ax0, ay0, ax1, ay1 = ART_BOX
|
||||||
|
scale = min((ax1 - ax0) / vbw, (ay1 - ay0) / vbh)
|
||||||
|
ox = ax0 + ((ax1 - ax0) - vbw * scale) / 2.0
|
||||||
|
oy = ay0 + ((ay1 - ay0) - vbh * scale) / 2.0
|
||||||
|
|
||||||
|
def tf(p):
|
||||||
|
# SVG y grows down, font y grows up.
|
||||||
|
return (ox + (p[0] - vbx) * scale, oy + (vbh - (p[1] - vby)) * scale)
|
||||||
|
|
||||||
|
contours = []
|
||||||
|
for sub in parse_path(path_d):
|
||||||
|
pts, cursor = [], None
|
||||||
|
for seg in sub:
|
||||||
|
if seg[0] in ('M', 'L'):
|
||||||
|
cursor = tf(seg[1])
|
||||||
|
pts.append((cursor[0], cursor[1], True))
|
||||||
|
elif seg[0] == 'C':
|
||||||
|
c1, c2, p = tf(seg[1]), tf(seg[2]), tf(seg[3])
|
||||||
|
for q, end in cubic_to_quads(cursor, c1, c2, p, TOL):
|
||||||
|
pts.append((q[0], q[1], False))
|
||||||
|
pts.append((end[0], end[1], True))
|
||||||
|
cursor = p
|
||||||
|
if len(pts) > 2:
|
||||||
|
contours.append(clean(pts))
|
||||||
|
return orient(contours)
|
||||||
|
|
||||||
|
|
||||||
|
def clean(pts):
|
||||||
|
"""Round to integers, drop duplicates and the redundant closing point."""
|
||||||
|
out = []
|
||||||
|
for x, y, on in pts:
|
||||||
|
p = (int(round(x)), int(round(y)), on)
|
||||||
|
if not out or out[-1] != p:
|
||||||
|
out.append(p)
|
||||||
|
while len(out) > 1 and out[-1][:2] == out[0][:2] and out[-1][2] and out[0][2]:
|
||||||
|
out.pop()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def area(pts):
|
||||||
|
a = 0.0
|
||||||
|
for i in range(len(pts)):
|
||||||
|
x0, y0 = pts[i][0], pts[i][1]
|
||||||
|
x1, y1 = pts[(i + 1) % len(pts)][0], pts[(i + 1) % len(pts)][1]
|
||||||
|
a += x0 * y1 - x1 * y0
|
||||||
|
return a / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def orient(contours):
|
||||||
|
"""TrueType draws outer contours clockwise (negative shoelace area).
|
||||||
|
|
||||||
|
Flipping the whole set preserves each hole's direction relative to its
|
||||||
|
outer contour, which is what keeps counters (eyes, cutouts) unfilled.
|
||||||
|
"""
|
||||||
|
if contours and area(max(contours, key=lambda c: abs(area(c)))) > 0:
|
||||||
|
return [list(reversed(c)) for c in contours]
|
||||||
|
return contours
|
||||||
|
|
||||||
|
|
||||||
|
# --- glyph and table encoding ----------------------------------------------
|
||||||
|
|
||||||
|
def encode_glyph(contours):
|
||||||
|
if not contours:
|
||||||
|
return b''
|
||||||
|
xs = [p[0] for c in contours for p in c]
|
||||||
|
ys = [p[1] for c in contours for p in c]
|
||||||
|
out = [struct.pack('>hhhhh', len(contours), min(xs), min(ys), max(xs), max(ys))]
|
||||||
|
ends, n = [], 0
|
||||||
|
for c in contours:
|
||||||
|
n += len(c)
|
||||||
|
ends.append(n - 1)
|
||||||
|
out.append(struct.pack('>%dH' % len(ends), *ends))
|
||||||
|
out.append(struct.pack('>H', 0)) # no instructions
|
||||||
|
|
||||||
|
flags, xdel, ydel = [], [], []
|
||||||
|
px = py = 0
|
||||||
|
for c in contours:
|
||||||
|
for x, y, on in c:
|
||||||
|
dx, dy = x - px, y - py
|
||||||
|
px, py = x, y
|
||||||
|
f = 1 if on else 0
|
||||||
|
if dx == 0:
|
||||||
|
f |= 0x10
|
||||||
|
elif -255 <= dx <= 255:
|
||||||
|
f |= 0x02 | (0x10 if dx > 0 else 0)
|
||||||
|
xdel.append(struct.pack('>B', abs(dx)))
|
||||||
|
else:
|
||||||
|
xdel.append(struct.pack('>h', dx))
|
||||||
|
if dy == 0:
|
||||||
|
f |= 0x20
|
||||||
|
elif -255 <= dy <= 255:
|
||||||
|
f |= 0x04 | (0x20 if dy > 0 else 0)
|
||||||
|
ydel.append(struct.pack('>B', abs(dy)))
|
||||||
|
else:
|
||||||
|
ydel.append(struct.pack('>h', dy))
|
||||||
|
flags.append(struct.pack('>B', f))
|
||||||
|
data = b''.join(out + flags + xdel + ydel)
|
||||||
|
return data + b'\0' * (-len(data) % 4)
|
||||||
|
|
||||||
|
|
||||||
|
def read_tables(d):
|
||||||
|
t = {}
|
||||||
|
for i in range(struct.unpack('>H', d[4:6])[0]):
|
||||||
|
off = 12 + i * 16
|
||||||
|
tag = d[off:off + 4].decode('latin-1')
|
||||||
|
s, l = struct.unpack('>II', d[off + 8:off + 16])
|
||||||
|
t[tag] = (s, l)
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def checksum(data):
|
||||||
|
data += b'\0' * (-len(data) % 4)
|
||||||
|
return sum(struct.unpack('>%dI' % (len(data) // 4), data)) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
def read_loca(d, t, ng):
|
||||||
|
ls = t['loca'][0]
|
||||||
|
if struct.unpack('>h', d[t['head'][0] + 50:t['head'][0] + 52])[0] == 0:
|
||||||
|
return [x * 2 for x in struct.unpack('>%dH' % (ng + 1), d[ls:ls + 2 * (ng + 1)])]
|
||||||
|
return list(struct.unpack('>%dI' % (ng + 1), d[ls:ls + 4 * (ng + 1)]))
|
||||||
|
|
||||||
|
|
||||||
|
def read_cmap(d, cs):
|
||||||
|
"""Read the format 12 subtable; it carries every mapping the font has."""
|
||||||
|
m = {}
|
||||||
|
for i in range(struct.unpack('>H', d[cs + 2:cs + 4])[0]):
|
||||||
|
pid, eid, off = struct.unpack('>HHI', d[cs + 4 + i * 8:cs + 12 + i * 8])
|
||||||
|
sub = cs + off
|
||||||
|
if struct.unpack('>H', d[sub:sub + 2])[0] != 12:
|
||||||
|
continue
|
||||||
|
for j in range(struct.unpack('>I', d[sub + 12:sub + 16])[0]):
|
||||||
|
s, e, g = struct.unpack('>III', d[sub + 16 + j * 12:sub + 28 + j * 12])
|
||||||
|
for c in range(s, e + 1):
|
||||||
|
m[c] = g + (c - s)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def read_names(d, ps, ng):
|
||||||
|
n = struct.unpack('>H', d[ps + 32:ps + 34])[0]
|
||||||
|
idx = list(struct.unpack('>%dH' % n, d[ps + 34:ps + 34 + n * 2]))
|
||||||
|
p = ps + 34 + n * 2
|
||||||
|
pool = []
|
||||||
|
need = max([i - 258 + 1 for i in idx if i >= 258] or [0])
|
||||||
|
while len(pool) < need:
|
||||||
|
ln = d[p]
|
||||||
|
pool.append(d[p + 1:p + 1 + ln].decode('latin-1'))
|
||||||
|
p += 1 + ln
|
||||||
|
out = [pool[i - 258] if i >= 258 else '#mac%d' % i for i in idx]
|
||||||
|
return out + ['#mac0'] * (ng - len(out))
|
||||||
|
|
||||||
|
|
||||||
|
def build_post(d, ps, names):
|
||||||
|
idx, pool = [], []
|
||||||
|
for nm in names:
|
||||||
|
if nm.startswith('#mac'):
|
||||||
|
idx.append(int(nm[4:]))
|
||||||
|
else:
|
||||||
|
idx.append(258 + len(pool))
|
||||||
|
pool.append(nm)
|
||||||
|
body = struct.pack('>H', len(idx)) + struct.pack('>%dH' % len(idx), *idx)
|
||||||
|
for nm in pool:
|
||||||
|
body += struct.pack('>B', len(nm)) + nm.encode('latin-1')
|
||||||
|
return d[ps:ps + 32] + body
|
||||||
|
|
||||||
|
|
||||||
|
def build_cmap(m):
|
||||||
|
"""Emit format 4 and 12 subtables under the usual encoding records."""
|
||||||
|
groups = []
|
||||||
|
for c in sorted(m):
|
||||||
|
if groups and c == groups[-1][1] + 1 and m[c] == groups[-1][2] + (c - groups[-1][0]):
|
||||||
|
groups[-1][1] = c
|
||||||
|
else:
|
||||||
|
groups.append([c, c, m[c]])
|
||||||
|
|
||||||
|
segs = [(a, b, g) for a, b, g in groups if b <= 0xFFFF] + [(0xFFFF, 0xFFFF, 0)]
|
||||||
|
count = len(segs)
|
||||||
|
pow2 = 2 ** (count.bit_length() - 1) * 2
|
||||||
|
f4 = struct.pack('>HHHHHHH', 4, 16 + count * 8, 0, count * 2,
|
||||||
|
pow2, count.bit_length() - 1, count * 2 - pow2)
|
||||||
|
f4 += struct.pack('>%dH' % count, *[s[1] for s in segs])
|
||||||
|
f4 += struct.pack('>H', 0)
|
||||||
|
f4 += struct.pack('>%dH' % count, *[s[0] for s in segs])
|
||||||
|
deltas = [((s[2] - s[0]) & 0xFFFF) if s[0] != 0xFFFF else 1 for s in segs]
|
||||||
|
f4 += struct.pack('>%dh' % count, *[x - 65536 if x > 32767 else x for x in deltas])
|
||||||
|
f4 += struct.pack('>%dH' % count, *([0] * count))
|
||||||
|
|
||||||
|
f12 = struct.pack('>HHIII', 12, 0, 16 + len(groups) * 12, 0, len(groups))
|
||||||
|
for a, b, g in groups:
|
||||||
|
f12 += struct.pack('>III', a, b, g)
|
||||||
|
|
||||||
|
f0 = struct.pack('>HHH', 0, 262, 0) + bytes(256)
|
||||||
|
|
||||||
|
body, offsets = b'', {}
|
||||||
|
for data in (f4, f12, f0):
|
||||||
|
offsets[id(data)] = len(body)
|
||||||
|
body += data
|
||||||
|
records = [(0, 3, f4), (0, 4, f12), (1, 0, f0), (3, 1, f4), (3, 10, f12)]
|
||||||
|
head = struct.pack('>HH', 0, len(records))
|
||||||
|
base = 4 + len(records) * 8
|
||||||
|
for pid, eid, data in records:
|
||||||
|
head += struct.pack('>HHI', pid, eid, base + offsets[id(data)])
|
||||||
|
return head + body
|
||||||
|
|
||||||
|
|
||||||
|
def assemble(tables):
|
||||||
|
tags = sorted(tables)
|
||||||
|
n = len(tags)
|
||||||
|
sr = 2 ** (n.bit_length() - 1) * 16
|
||||||
|
font = struct.pack('>IHHHH', 0x00010000, n, sr, n.bit_length() - 1, n * 16 - sr)
|
||||||
|
offset = 12 + n * 16
|
||||||
|
body, records = b'', []
|
||||||
|
for tag in tags:
|
||||||
|
data = tables[tag]
|
||||||
|
records.append((tag, checksum(data), offset + len(body), len(data)))
|
||||||
|
body += data + b'\0' * (-len(data) % 4)
|
||||||
|
font += b''.join(struct.pack('>4sIII', t.encode('latin-1'), c, o, l)
|
||||||
|
for t, c, o, l in records)
|
||||||
|
font += body
|
||||||
|
adj = (0xB1B0AFBA - checksum(font)) & 0xFFFFFFFF
|
||||||
|
hs = [o for t, c, o, l in records if t == 'head'][0]
|
||||||
|
return font[:hs + 8] + struct.pack('>I', adj) + font[hs + 12:]
|
||||||
|
|
||||||
|
|
||||||
|
# --- commands --------------------------------------------------------------
|
||||||
|
|
||||||
|
def load_font(path):
|
||||||
|
d = open(path, 'rb').read()
|
||||||
|
t = read_tables(d)
|
||||||
|
ng = struct.unpack('>H', d[t['maxp'][0] + 4:t['maxp'][0] + 6])[0]
|
||||||
|
return d, t, ng
|
||||||
|
|
||||||
|
|
||||||
|
def glyph_list(path):
|
||||||
|
d, t, ng = load_font(path)
|
||||||
|
loca = read_loca(d, t, ng)
|
||||||
|
names = read_names(d, t['post'][0], ng)
|
||||||
|
gs = t['glyf'][0]
|
||||||
|
rows = []
|
||||||
|
for cp, gid in sorted(read_cmap(d, t['cmap'][0]).items()):
|
||||||
|
if cp < PUA_FIRST:
|
||||||
|
continue
|
||||||
|
a, b = loca[gid], loca[gid + 1]
|
||||||
|
box = ''
|
||||||
|
if b > a:
|
||||||
|
_, x0, y0, x1, y1 = struct.unpack('>hhhhh', d[gs + a:gs + a + 10])
|
||||||
|
box = '%d x %d' % (x1 - x0, y1 - y0)
|
||||||
|
name = names[gid]
|
||||||
|
rows.append((cp, '' if name.startswith('#mac') else name, box))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def add_glyph(font_path, codepoint, name, contours):
|
||||||
|
d, t, ng = load_font(font_path)
|
||||||
|
loca = read_loca(d, t, ng)
|
||||||
|
gs = t['glyf'][0]
|
||||||
|
glyphs = [d[gs + loca[i]:gs + loca[i + 1]] for i in range(ng)]
|
||||||
|
|
||||||
|
nhm = struct.unpack('>H', d[t['hhea'][0] + 34:t['hhea'][0] + 36])[0]
|
||||||
|
hm = t['hmtx'][0]
|
||||||
|
adv, lsb = [], []
|
||||||
|
for i in range(ng):
|
||||||
|
if i < nhm:
|
||||||
|
a, b = struct.unpack('>Hh', d[hm + i * 4:hm + i * 4 + 4])
|
||||||
|
else:
|
||||||
|
a = adv[-1]
|
||||||
|
b = struct.unpack('>h', d[hm + nhm * 4 + (i - nhm) * 2:][:2])[0]
|
||||||
|
adv.append(a)
|
||||||
|
lsb.append(b)
|
||||||
|
|
||||||
|
names = read_names(d, t['post'][0], ng)
|
||||||
|
cmap = read_cmap(d, t['cmap'][0])
|
||||||
|
|
||||||
|
gid = len(glyphs)
|
||||||
|
glyphs.append(encode_glyph(contours))
|
||||||
|
xs = [p[0] for c in contours for p in c]
|
||||||
|
adv.append(UPEM)
|
||||||
|
lsb.append(min(xs) if xs else 0)
|
||||||
|
names.append(name)
|
||||||
|
cmap[codepoint] = gid
|
||||||
|
ng = len(glyphs)
|
||||||
|
|
||||||
|
offs, o = [0], 0
|
||||||
|
for g in glyphs:
|
||||||
|
o += len(g)
|
||||||
|
offs.append(o)
|
||||||
|
short = offs[-1] <= 0x1FFFE and all(x % 2 == 0 for x in offs)
|
||||||
|
|
||||||
|
hhea = bytearray(d[t['hhea'][0]:t['hhea'][0] + t['hhea'][1]])
|
||||||
|
struct.pack_into('>H', hhea, 34, ng)
|
||||||
|
|
||||||
|
maxp = bytearray(d[t['maxp'][0]:t['maxp'][0] + t['maxp'][1]])
|
||||||
|
struct.pack_into('>H', maxp, 4, ng)
|
||||||
|
struct.pack_into('>H', maxp, 6, max(struct.unpack('>H', bytes(maxp[6:8]))[0],
|
||||||
|
sum(len(c) for c in contours)))
|
||||||
|
struct.pack_into('>H', maxp, 8, max(struct.unpack('>H', bytes(maxp[8:10]))[0],
|
||||||
|
len(contours)))
|
||||||
|
|
||||||
|
boxes = [struct.unpack('>hhhhh', g[:10])[1:] for g in glyphs if len(g) >= 10]
|
||||||
|
head = bytearray(d[t['head'][0]:t['head'][0] + t['head'][1]])
|
||||||
|
struct.pack_into('>hhhh', head, 36,
|
||||||
|
min(b[0] for b in boxes), min(b[1] for b in boxes),
|
||||||
|
max(b[2] for b in boxes), max(b[3] for b in boxes))
|
||||||
|
struct.pack_into('>h', head, 50, 0 if short else 1)
|
||||||
|
struct.pack_into('>I', head, 8, 0) # checkSumAdjustment, recomputed below
|
||||||
|
|
||||||
|
tables = {tag: d[s:s + l] for tag, (s, l) in t.items()}
|
||||||
|
tables['glyf'] = b''.join(glyphs)
|
||||||
|
tables['loca'] = (struct.pack('>%dH' % len(offs), *[x // 2 for x in offs]) if short
|
||||||
|
else struct.pack('>%dI' % len(offs), *offs))
|
||||||
|
tables['hmtx'] = b''.join(struct.pack('>Hh', adv[i], lsb[i]) for i in range(ng))
|
||||||
|
tables['hhea'] = bytes(hhea)
|
||||||
|
tables['maxp'] = bytes(maxp)
|
||||||
|
tables['cmap'] = build_cmap(cmap)
|
||||||
|
tables['post'] = build_post(d, t['post'][0], names)
|
||||||
|
tables['head'] = bytes(head)
|
||||||
|
|
||||||
|
open(font_path, 'wb').write(assemble(tables))
|
||||||
|
|
||||||
|
|
||||||
|
def read_svg(location):
|
||||||
|
if not location.startswith(('http://', 'https://')):
|
||||||
|
return open(location, encoding='utf-8').read()
|
||||||
|
# Icon sites reject the stock urllib agent, so ask like a browser would.
|
||||||
|
req = urllib.request.Request(location, headers={'User-Agent': 'omarchy-dev-font'})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return r.read().decode('utf-8')
|
||||||
|
except OSError as e:
|
||||||
|
sys.exit('could not fetch %s: %s' % (location, e))
|
||||||
|
|
||||||
|
|
||||||
|
def svg_contours(svg, location):
|
||||||
|
vb = re.search(r'viewBox="([^"]+)"', svg)
|
||||||
|
if not vb:
|
||||||
|
sys.exit('%s: no viewBox; need an SVG with a viewBox' % location)
|
||||||
|
view = tuple(float(x) for x in vb.group(1).replace(',', ' ').split())
|
||||||
|
paths = re.findall(r'<path[^>]*\bd="([^"]+)"', svg)
|
||||||
|
if len(paths) != 1:
|
||||||
|
sys.exit('%s: expected a single <path>, found %d — flatten the mark to '
|
||||||
|
'one monochrome path first' % (location, len(paths)))
|
||||||
|
return contours_from_svg(paths[0], view)
|
||||||
|
|
||||||
|
|
||||||
|
def default_font():
|
||||||
|
root = os.environ.get('OMARCHY_PATH')
|
||||||
|
if not root:
|
||||||
|
sys.exit('OMARCHY_PATH is not set')
|
||||||
|
return os.path.join(root, 'default/fonts/omarchy/omarchy.ttf')
|
||||||
|
|
||||||
|
|
||||||
|
def readme_for(font_path):
|
||||||
|
return os.path.join(os.path.dirname(font_path), 'README.md')
|
||||||
|
|
||||||
|
|
||||||
|
def record_source(font_path, codepoint, label, location):
|
||||||
|
"""Append the new mark to the font's README so sources stay tracked."""
|
||||||
|
readme = readme_for(font_path)
|
||||||
|
if not os.path.exists(readme):
|
||||||
|
return None
|
||||||
|
lines = open(readme, encoding='utf-8').read().split('\n')
|
||||||
|
last = max(i for i, l in enumerate(lines) if l.startswith('- `U+'))
|
||||||
|
entry = '- `U+%04X` — %s' % (codepoint, label)
|
||||||
|
if location.startswith(('http://', 'https://')):
|
||||||
|
entry += ', from <%s>' % location
|
||||||
|
lines.insert(last + 1, entry)
|
||||||
|
open(readme, 'w', encoding='utf-8').write('\n'.join(lines))
|
||||||
|
return readme
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
common = argparse.ArgumentParser(add_help=False)
|
||||||
|
common.add_argument('--font', help='font to edit '
|
||||||
|
'(default: $OMARCHY_PATH/default/fonts/omarchy/omarchy.ttf)')
|
||||||
|
|
||||||
|
p = argparse.ArgumentParser(prog='omarchy dev font', parents=[common],
|
||||||
|
description=__doc__.split('\n')[0])
|
||||||
|
sub = p.add_subparsers(dest='command')
|
||||||
|
sub.add_parser('list', parents=[common], help='show the marks the font carries')
|
||||||
|
add = sub.add_parser('add', parents=[common],
|
||||||
|
help='append a mark from a monochrome SVG')
|
||||||
|
add.add_argument('name', help='glyph name, e.g. ollama')
|
||||||
|
add.add_argument('svg', help='SVG file or URL')
|
||||||
|
add.add_argument('--codepoint', help='private-use codepoint (default: next free)')
|
||||||
|
add.add_argument('--label', help='README label (default: the glyph name)')
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
font = args.font or default_font()
|
||||||
|
if not os.path.exists(font):
|
||||||
|
sys.exit('no font at %s' % font)
|
||||||
|
|
||||||
|
if args.command in (None, 'list'):
|
||||||
|
for cp, name, box in glyph_list(font):
|
||||||
|
print('U+%04X %s %-12s %s' % (cp, chr(cp), name, box))
|
||||||
|
return
|
||||||
|
|
||||||
|
taken = {cp for cp, _, _ in glyph_list(font)}
|
||||||
|
if args.codepoint:
|
||||||
|
cp = int(args.codepoint.upper().replace('U+', ''), 16)
|
||||||
|
if cp in taken:
|
||||||
|
sys.exit('U+%04X is already used' % cp)
|
||||||
|
else:
|
||||||
|
cp = max(taken) + 1 if taken else PUA_FIRST
|
||||||
|
|
||||||
|
contours = svg_contours(read_svg(args.svg), args.svg)
|
||||||
|
add_glyph(font, cp, args.name, contours)
|
||||||
|
readme = record_source(font, cp, args.label or args.name, args.svg)
|
||||||
|
|
||||||
|
print('Added %s as U+%04X (%s)' % (args.name, cp, chr(cp)))
|
||||||
|
print()
|
||||||
|
print('Next:')
|
||||||
|
if readme:
|
||||||
|
print(' - check the new line in %s' % os.path.relpath(readme))
|
||||||
|
print(' - point a menu entry at it: "icon":"%s","iconFont":"omarchy"' % chr(cp))
|
||||||
|
print(' - bump the charset range in test/shell.d/menu-test.sh to e900-%x' % cp)
|
||||||
|
print(' - the font is package-owned, so it reaches the desktop through an')
|
||||||
|
print(' omarchy-settings release, not through omarchy update')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
+32
-7
@@ -6,6 +6,18 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Whenever this runs as root — invoked directly through the passwordless
|
||||||
|
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
|
||||||
|
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
|
||||||
|
# prepends a user-writable checkout bin/ to it. Every helper this script calls
|
||||||
|
# by bare name (dirname, install, tee, rm, nmcli, systemctl, awk) is a system
|
||||||
|
# tool, never an omarchy-* command, so pin PATH to trusted system directories
|
||||||
|
# and keep root from resolving one out of that checkout. The unprivileged
|
||||||
|
# wrapper phase keeps the caller's PATH so it can still find sudo/pkexec.
|
||||||
|
if (( EUID == 0 )); then
|
||||||
|
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
|
||||||
|
fi
|
||||||
|
|
||||||
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
|
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
|
||||||
|
|
||||||
provider_from_arg() {
|
provider_from_arg() {
|
||||||
@@ -28,19 +40,32 @@ provider_from_arg() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
self_path() {
|
# The path etc/sudoers.d/omarchy-dns names. The privileged half always runs from
|
||||||
readlink -f "$OMARCHY_PATH/bin/${BASH_SOURCE[0]##*/}"
|
# there rather than from whichever copy was invoked, so the rule matches even
|
||||||
|
# where $OMARCHY_PATH points at a checkout.
|
||||||
|
PACKAGED_PATH=/usr/bin/omarchy-dns
|
||||||
|
|
||||||
|
# True when sudo would run this exact command without stopping for a password.
|
||||||
|
# `sudo -l` on its own reports whether a command is permitted, which the blanket
|
||||||
|
# %wheel rule answers yes to for everything; the long listing prints the matched
|
||||||
|
# entry's tags, so !authenticate is the grant in etc/sudoers.d/omarchy-dns and
|
||||||
|
# nothing else. Listing runs nothing and, under -n, prompts for nothing, so a
|
||||||
|
# machine whose omarchy-settings predates that file falls through to polkit
|
||||||
|
# instead of dying on a password prompt it has no terminal to show.
|
||||||
|
sudo_grants_passwordless() {
|
||||||
|
sudo -n -l -l "$PACKAGED_PATH" "$@" 2>/dev/null | grep -q '!authenticate'
|
||||||
}
|
}
|
||||||
|
|
||||||
require_root() {
|
require_root() {
|
||||||
if (( EUID == 0 )); then
|
if (( EUID == 0 )); then
|
||||||
return
|
return
|
||||||
fi
|
elif [[ -t 0 ]] || sudo_grants_passwordless "$@"; then
|
||||||
|
# A terminal can carry sudo's own password prompt. Without one, sudo is
|
||||||
if [[ -t 0 ]]; then
|
# right only where the grant reaches; polkit can at least put a prompt on
|
||||||
exec sudo "$(self_path)" "$@"
|
# screen, and offer to authenticate as someone else.
|
||||||
|
exec sudo "$PACKAGED_PATH" "$@"
|
||||||
else
|
else
|
||||||
exec pkexec "$(self_path)" "$@"
|
exec pkexec "$PACKAGED_PATH" "$@"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Check that a git URL names a repository, not a transport helper
|
||||||
|
# omarchy:args=<git-url>
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# git picks a remote helper -- an executable it runs at clone time -- out of a URL
|
||||||
|
# in exactly two shapes, and no others: `<helper>::<address>`, and
|
||||||
|
# `<scheme>://<address>` for any scheme git does not handle itself. A single
|
||||||
|
# colon is always scp-style ssh, and a bare path is always a path; neither can
|
||||||
|
# reach a helper. So constraining those two shapes covers the whole surface.
|
||||||
|
#
|
||||||
|
# The `::` shape is refused outright, because no helper reachable that way is one
|
||||||
|
# a theme or plugin URL has business naming, and `ext::` runs a shell command.
|
||||||
|
# The `://` shape cannot be refused the same way, since it is also how every
|
||||||
|
# legitimate URL arrives -- so it is allowlisted instead. The list is the
|
||||||
|
# transports git still connects itself, `git+ssh` and `ssh+git` included: those
|
||||||
|
# two are spelled like a helper but are read as plain ssh. `ext` and `fd` are
|
||||||
|
# left out deliberately -- git ships a helper for each, and `ext` runs whatever
|
||||||
|
# command the URL carries.
|
||||||
|
TRANSPORTS=(ssh git git+ssh ssh+git http https ftp ftps file)
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "omarchy-git-url-check: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
url="${1-}"
|
||||||
|
|
||||||
|
if [[ -z $url ]]; then
|
||||||
|
fail "a git URL is required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||||
|
fail "'$url' names a git option or transport helper, not a repository."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $url =~ ^([A-Za-z0-9][A-Za-z0-9+.-]*):// ]]; then
|
||||||
|
scheme="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
|
for transport in "${TRANSPORTS[@]}"; do
|
||||||
|
if [[ $scheme == "$transport" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
fail "'$url' names the '$scheme' transport, which Omarchy does not clone from."
|
||||||
|
fi
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
# omarchy:summary=Returns true when an external monitor is physically connected.
|
# omarchy:summary=Returns true when an external monitor is physically connected.
|
||||||
|
|
||||||
for status in /sys/class/drm/card*-*/status; do
|
drm_path="${OMARCHY_DRM_PATH:-/sys/class/drm}"
|
||||||
[[ $status == *-eDP-*/status ]] && continue
|
|
||||||
|
for status in "$drm_path"/card*-*/status; do
|
||||||
|
[[ -e $status ]] || continue
|
||||||
|
[[ $status =~ -(eDP|LVDS|DSI)-[^/]+/status$ ]] && continue
|
||||||
[[ $(< $status) == "connected" ]] && exit 0
|
[[ $(< $status) == "connected" ]] && exit 0
|
||||||
done
|
done
|
||||||
exit 1
|
exit 1
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
# also make USB touchscreens) are left out to avoid nagging laptops with no
|
# also make USB touchscreens) are left out to avoid nagging laptops with no
|
||||||
# reader — those still match on the product string below when present.
|
# reader — those still match on the product string below when present.
|
||||||
fingerprint_vendors=" 27c6 138a 06cb 08ff 1c7a 147e "
|
fingerprint_vendors=" 27c6 138a 06cb 08ff 1c7a 147e "
|
||||||
|
usb_devices_path="${OMARCHY_USB_DEVICES_PATH:-/sys/bus/usb/devices}"
|
||||||
|
|
||||||
# libfprint drives every reader it supports from userspace over libusb, so a
|
# libfprint drives every reader it supports from userspace over libusb, so a
|
||||||
# real reader sits there with no kernel driver bound to any of its interfaces.
|
# real reader sits there with no kernel driver bound to any of its interfaces.
|
||||||
@@ -17,23 +18,32 @@ fingerprint_vendors=" 27c6 138a 06cb 08ff 1c7a 147e "
|
|||||||
# — all bind one. Only the vendor-ID guess needs this; a device that names
|
# — all bind one. Only the vendor-ID guess needs this; a device that names
|
||||||
# itself a fingerprint reader is trusted outright.
|
# itself a fingerprint reader is trusted outright.
|
||||||
has_kernel_driver() {
|
has_kernel_driver() {
|
||||||
local intf
|
local intf driver
|
||||||
for intf in "$1"/*:*; do
|
for intf in "$1"/*:*; do
|
||||||
[[ -e $intf/driver ]] && return 0
|
[[ -e $intf/driver ]] || continue
|
||||||
|
# usbfs is the exception: libusb claims an interface through it, so a reader
|
||||||
|
# fprintd is enrolling or verifying against binds a driver for as long as it
|
||||||
|
# holds the claim. That is userspace driving the device — what a reader is
|
||||||
|
# supposed to look like — so it must not read as a kernel driver here.
|
||||||
|
driver=$(readlink -f "$intf/driver")
|
||||||
|
[[ ${driver##*/} == "usbfs" ]] || return 0
|
||||||
done
|
done
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for dev in /sys/bus/usb/devices/*; do
|
for dev in "$usb_devices_path"/*; do
|
||||||
# The device's own product descriptor usually names it, e.g. "Goodix
|
# The device's own product descriptor usually names it, e.g. "Goodix
|
||||||
# Fingerprint USB Device" — driver-independent and vendor-agnostic.
|
# Fingerprint USB Device" — driver-independent and vendor-agnostic.
|
||||||
if [[ -r $dev/product ]]; then
|
if [[ -r $dev/product ]]; then
|
||||||
product=$(<"$dev/product")
|
product=$(<"$dev/product")
|
||||||
product=${product,,}
|
product=${product,,}
|
||||||
# Elan's match-on-chip readers report "ELAN:ARM-M4", the family name
|
# Elan's match-on-chip readers report "ELAN:ARM-M4" and Fingerprint Cards'
|
||||||
# rather than the function. Elan is left out of the vendor list above
|
# report "FPC Sensor Controller" or "FPC L:0000 FW:1425046" — the family or
|
||||||
# on purpose, so without this they match nothing.
|
# the manufacturer rather than the function. Both vendors are left out of
|
||||||
[[ $product == *fingerprint* || $product == *biometric* || $product == *elan:arm-m4* ]] && exit 0
|
# the list above on purpose (Elan also makes touchscreens), so without these
|
||||||
|
# they match nothing. FPC leads the string on every reader on record, and
|
||||||
|
# three letters are little to match on, so require the prefix.
|
||||||
|
[[ $product == *fingerprint* || $product == *biometric* || $product == *elan:arm-m4* || $product == "fpc "* ]] && exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -r $dev/idVendor ]]; then
|
if [[ -r $dev/idVendor ]]; then
|
||||||
|
|||||||
@@ -2,8 +2,23 @@
|
|||||||
|
|
||||||
# omarchy:summary=Detect whether the system has an active hybrid GPU configuration
|
# omarchy:summary=Detect whether the system has an active hybrid GPU configuration
|
||||||
|
|
||||||
if omarchy-cmd-present supergfxctl; then
|
multiple_gpus() {
|
||||||
supergfxctl -s 2>/dev/null | grep -qw Hybrid
|
|
||||||
else
|
|
||||||
(($(lspci | grep -cE 'VGA|3D|Display') >= 2))
|
(($(lspci | grep -cE 'VGA|3D|Display') >= 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
if omarchy-cmd-present supergfxctl; then
|
||||||
|
# A wedged supergfxd blocks its clients forever, and this gate runs while
|
||||||
|
# the menu renders. Bound the query, and treat a daemon that cannot answer
|
||||||
|
# like a machine without supergfxctl: count GPUs instead of hiding hardware
|
||||||
|
# that is really there.
|
||||||
|
modes=$(timeout --kill-after=1s 1s supergfxctl -s 2>/dev/null)
|
||||||
|
status=$?
|
||||||
|
|
||||||
|
if ((status == 124 || status == 137)); then
|
||||||
|
multiple_gpus
|
||||||
|
else
|
||||||
|
grep -qw Hybrid <<<"$modes"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
multiple_gpus
|
||||||
fi
|
fi
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
# omarchy:summary=Check whether a webcam is available
|
# omarchy:summary=Check whether a webcam is available
|
||||||
|
|
||||||
v4l2-ctl --list-devices 2>/dev/null | grep -q '^[[:space:]]*/dev/video'
|
[[ -n $(omarchy-capture-webcam-list) ]]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Focus a Hyprland window by application class
|
# omarchy:summary=Focus a Hyprland window by application identity
|
||||||
# omarchy:args=<app-name>
|
# omarchy:args=<app-name>
|
||||||
# omarchy:examples=omarchy hyprland focus app Slack
|
# omarchy:examples=omarchy hyprland focus app Slack
|
||||||
|
|
||||||
@@ -12,10 +12,17 @@ usage() {
|
|||||||
app=${1:-}
|
app=${1:-}
|
||||||
[[ -n $app ]] || usage
|
[[ -n $app ]] || usage
|
||||||
|
|
||||||
|
# Agent terminals notify as kitty/foot/etc. while their shared window class is
|
||||||
|
# org.omarchy.agent, leaving the terminal name only in initialTitle. So match
|
||||||
|
# by class first, then fall back to the launch-time title of agent windows.
|
||||||
address=$(
|
address=$(
|
||||||
hyprctl clients -j 2>/dev/null |
|
hyprctl clients -j 2>/dev/null |
|
||||||
jq -r --arg pattern "$app" \
|
jq -r --arg pattern "$app" \
|
||||||
'[.[] | select((.class // "") | test($pattern; "i"))] | first.address // empty'
|
'def matches($value): ($value // "") | test($pattern; "i");
|
||||||
|
first(
|
||||||
|
(.[] | select(matches(.class))),
|
||||||
|
(.[] | select(.initialClass == "org.omarchy.agent" and matches(.initialTitle)))
|
||||||
|
).address // empty'
|
||||||
)
|
)
|
||||||
|
|
||||||
[[ -n $address ]] || exit 1
|
[[ -n $address ]] || exit 1
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ MONITOR_LUA="$HOME/.config/hypr/monitors.lua"
|
|||||||
|
|
||||||
INTERNAL=$(omarchy-hyprland-monitor-laptop)
|
INTERNAL=$(omarchy-hyprland-monitor-laptop)
|
||||||
|
|
||||||
|
# INTERNAL is written into generated Lua and hyprctl eval/dispatch below, so a
|
||||||
|
# name that is not a plain connector string could execute on the next reload.
|
||||||
|
# Names come from hyprctl; a user-created headless output can carry anything.
|
||||||
|
if [[ -n $INTERNAL && ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
echo "Refusing unsafe internal monitor name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
valid_scale() {
|
valid_scale() {
|
||||||
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
|
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
|
||||||
}
|
}
|
||||||
@@ -118,9 +126,17 @@ remember_internal_scale() {
|
|||||||
store_internal_scale "$scale"
|
store_internal_scale "$scale"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# $1 is the configured scale when the caller has already read it, so one sync
|
||||||
|
# does not parse the config twice.
|
||||||
read_monitor_scale() {
|
read_monitor_scale() {
|
||||||
local scale
|
local scale
|
||||||
scale=$(configured_monitor_scale)
|
|
||||||
|
if (( $# )); then
|
||||||
|
scale="$1"
|
||||||
|
else
|
||||||
|
scale=$(configured_monitor_scale)
|
||||||
|
fi
|
||||||
|
|
||||||
if valid_scale "$scale"; then
|
if valid_scale "$scale"; then
|
||||||
echo "$scale"
|
echo "$scale"
|
||||||
return
|
return
|
||||||
@@ -159,11 +175,25 @@ enable_internal_output() {
|
|||||||
|
|
||||||
sync_internal_scale() {
|
sync_internal_scale() {
|
||||||
[[ -n $INTERNAL ]] || return 0
|
[[ -n $INTERNAL ]] || return 0
|
||||||
|
local configured_scale
|
||||||
local desired_scale
|
local desired_scale
|
||||||
local active_scale
|
local active_scale
|
||||||
|
|
||||||
desired_scale=$(read_monitor_scale)
|
configured_scale=$(configured_monitor_scale)
|
||||||
active_scale=$(current_internal_scale)
|
active_scale=$(current_internal_scale)
|
||||||
|
|
||||||
|
# A config without a usable number -- the default "auto", or an expression
|
||||||
|
# only Hyprland's Lua can evaluate -- delegates the scale to the compositor,
|
||||||
|
# so whatever it resolved for the enabled panel IS the configured scale.
|
||||||
|
# There is no number to correct it toward: substituting one makes the scale
|
||||||
|
# flap between that number and the compositor's own value on every idle-wake.
|
||||||
|
# Only a panel that is off altogether still gets a hand below, from the
|
||||||
|
# remembered scale.
|
||||||
|
if ! valid_scale "$configured_scale" && valid_scale "$active_scale"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
desired_scale=$(read_monitor_scale "$configured_scale")
|
||||||
scales_match "$active_scale" "$desired_scale" && return 0
|
scales_match "$active_scale" "$desired_scale" && return 0
|
||||||
|
|
||||||
enable_internal_output "$desired_scale"
|
enable_internal_output "$desired_scale"
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ off() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The name is written into generated Lua below, so only a plain connector
|
||||||
|
# name may pass; anything else could execute on the next reload.
|
||||||
|
if [[ ! $INTERNAL =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if ! omarchy-hyprland-monitor-external-active; then
|
if ! omarchy-hyprland-monitor-external-active; then
|
||||||
omarchy-notification-send -g "Can't disable the only active display"
|
omarchy-notification-send -g "Can't disable the only active display"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ on() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Both names are written into generated Lua below, so only plain connector
|
||||||
|
# names may pass; a user-created headless output can carry any name.
|
||||||
|
for output in "$INTERNAL" "$EXTERNAL"; do
|
||||||
|
if [[ ! $output =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
omarchy-notification-send -g "Refusing unsafe monitor name"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
omarchy-hyprland-toggle $DISABLE_TOGGLE off
|
omarchy-hyprland-toggle $DISABLE_TOGGLE off
|
||||||
|
|
||||||
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
if omarchy-hyprland-toggle-disabled $TOGGLE; then
|
||||||
|
|||||||
@@ -80,6 +80,14 @@ set_scale() {
|
|||||||
local width="$(echo "$monitor_info" | jq -r '.width')"
|
local width="$(echo "$monitor_info" | jq -r '.width')"
|
||||||
local height="$(echo "$monitor_info" | jq -r '.height')"
|
local height="$(echo "$monitor_info" | jq -r '.height')"
|
||||||
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
|
local refresh_rate="$(echo "$monitor_info" | jq -r '.refreshRate')"
|
||||||
|
|
||||||
|
# active_monitor is written into the Lua string eval'd below, so only a plain
|
||||||
|
# connector name may pass; a hostile output name could execute otherwise.
|
||||||
|
if [[ ! $active_monitor =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||||
|
echo "Refusing unsafe monitor name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
|
local new_scale="$(clean_scale "$requested_scale" "$width" "$height")"
|
||||||
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
|
# GTK only honors integer GDK_SCALE values, so persist the nearest whole
|
||||||
# factor even when the monitor scale itself is fractional.
|
# factor even when the monitor scale itself is fractional.
|
||||||
|
|||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Install the ChatGPT desktop app
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Installing ChatGPT..."
|
||||||
|
omarchy-pkg-add openai-codex-desktop
|
||||||
|
|
||||||
|
echo "Opening ChatGPT..."
|
||||||
|
setsid uwsm-app -- /usr/bin/chatgpt >/dev/null 2>&1 &
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "ChatGPT has been installed."
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Restore the preinstalled Omarchy applications (web apps, TUIs, and selected packages).
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
if gum confirm "Are you sure you want to restore all preinstalled web apps, TUI wrappers, and desktop applications?"; then
|
||||||
|
echo -e "Restoring preinstalled Omarchy applications...\n"
|
||||||
|
|
||||||
|
# Recreates the shipped .desktop launchers (web apps and TUIs) and the mise stubs
|
||||||
|
# that back claude, gh, opencode, and the rest of the agents
|
||||||
|
omarchy-refresh-applications
|
||||||
|
|
||||||
|
# Mirrors the list in omarchy-remove-preinstalls; both track omarchy-base.packages
|
||||||
|
if ! omarchy-pkg-add \
|
||||||
|
aether \
|
||||||
|
cliamp \
|
||||||
|
libreoffice-fresh \
|
||||||
|
xournalpp \
|
||||||
|
pinta \
|
||||||
|
obsidian \
|
||||||
|
obs-studio \
|
||||||
|
kdenlive \
|
||||||
|
moonlight-qt \
|
||||||
|
lazydocker \
|
||||||
|
omacut \
|
||||||
|
omacalc \
|
||||||
|
omawrite; then
|
||||||
|
echo -e "\nPreinstalls are still marked as removed. Fix the errors above and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Last, so a failure above leaves the opt-out intact rather than restoring
|
||||||
|
# keybindings for apps that never came back
|
||||||
|
rm -f ~/.local/state/omarchy/preinstalls-removed
|
||||||
|
hyprctl reload
|
||||||
|
fi
|
||||||
+132
-40
@@ -2,13 +2,32 @@
|
|||||||
|
|
||||||
# omarchy:summary=Launch the fastfetch TUI that gives information about the current system.
|
# omarchy:summary=Launch the fastfetch TUI that gives information about the current system.
|
||||||
|
|
||||||
# The float rule only sets a starting size: the exact fit depends on the terminal
|
# The size that hugs the About content depends on the terminal font and the user's
|
||||||
# font and the user's About logo, so once the terminal grid has settled we measure
|
# logo, so it can only be measured from inside the terminal. We remember the size
|
||||||
# the rendered content and resize the window to hug it with even padding all around.
|
# that fit and apply it as a window rule before launching, so the window opens at
|
||||||
# Bash defers WINCH traps while read blocks, so poll for size changes and re-render
|
# it instead of resizing after the first paint. Bash defers WINCH traps while read
|
||||||
# fastfetch at the new width whenever the window is resized.
|
# blocks, so poll for size changes and re-render fastfetch whenever it is resized.
|
||||||
|
|
||||||
LOGO_FILE="$HOME/.config/omarchy/branding/about.txt"
|
LOGO_FILE="$HOME/.config/omarchy/branding/about.txt"
|
||||||
|
FIT_FILE="$HOME/.local/state/omarchy/windows/about.fit"
|
||||||
|
|
||||||
|
# A user-level fastfetch config can relocate or restyle the logo in ways this
|
||||||
|
# measurement cannot see, so leave sizing to the float rule in that case.
|
||||||
|
custom_fastfetch_config() {
|
||||||
|
[[ -f $HOME/.config/fastfetch/config.jsonc ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# wc -L counts display columns only in a UTF-8 locale. A session that never set
|
||||||
|
# one counts every box-drawing and Nerd Font glyph in the About layout as
|
||||||
|
# nothing, which measures the content narrower than it renders.
|
||||||
|
display_columns() {
|
||||||
|
LC_ALL=C.UTF-8 wc -L
|
||||||
|
}
|
||||||
|
|
||||||
|
logo_dimensions() {
|
||||||
|
[[ -f $LOGO_FILE ]] || return 1
|
||||||
|
printf '%s %s' "$(display_columns <"$LOGO_FILE")" "$(wc -l <"$LOGO_FILE")"
|
||||||
|
}
|
||||||
|
|
||||||
hypr_dispatch() {
|
hypr_dispatch() {
|
||||||
local lua="$1"
|
local lua="$1"
|
||||||
@@ -16,74 +35,147 @@ hypr_dispatch() {
|
|||||||
hyprctl dispatch "$lua" >/dev/null 2>&1 || hyprctl dispatch "$@" >/dev/null
|
hyprctl dispatch "$lua" >/dev/null 2>&1 || hyprctl dispatch "$@" >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remember_fit() {
|
||||||
|
local directory tmp
|
||||||
|
directory=$(dirname "$FIT_FILE")
|
||||||
|
mkdir -p "$directory"
|
||||||
|
tmp=$(mktemp "$directory/.about.fit.XXXXXX")
|
||||||
|
printf '%s %s %s %s\n' "$1" "$2" "$3" "$4" >"$tmp"
|
||||||
|
mv "$tmp" "$FIT_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Replaces the rule from the last launch so a remembered size never outlives the
|
||||||
|
# fit it came from. Called without one it only clears, leaving the float rule's
|
||||||
|
# starting size — where a Hyprland without the Lua API stays too.
|
||||||
|
apply_size_rule() {
|
||||||
|
local rule=""
|
||||||
|
(( $# == 2 )) && rule="omarchy_about_size_rule = hl.window_rule({ match = { class = \"org.omarchy.about\" }, size = { $1, $2 } })"
|
||||||
|
|
||||||
|
hyprctl eval "if omarchy_about_size_rule then omarchy_about_size_rule:set_enabled(false) end; omarchy_about_size_rule = nil; $rule" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sized before the terminal is spawned, so the window maps at its final size.
|
||||||
|
presize_window() {
|
||||||
|
local logo_w logo_h fit_logo_w fit_logo_h fit_w fit_h
|
||||||
|
|
||||||
|
if ! custom_fastfetch_config && [[ -r $FIT_FILE ]]; then
|
||||||
|
read -r logo_w logo_h <<<"$(logo_dimensions)"
|
||||||
|
read -r fit_logo_w fit_logo_h fit_w fit_h <"$FIT_FILE"
|
||||||
|
|
||||||
|
# A different logo needs a different window, so leave that launch to the
|
||||||
|
# float rule and let the fit measure the new size.
|
||||||
|
if [[ -n ${logo_w:-} && $logo_w == "$fit_logo_w" && $logo_h == "$fit_logo_h" ]] &&
|
||||||
|
[[ $fit_w =~ ^[0-9]+$ && $fit_h =~ ^[0-9]+$ ]]; then
|
||||||
|
apply_size_rule "$fit_w" "$fit_h"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
apply_size_rule
|
||||||
|
}
|
||||||
|
|
||||||
|
# Hyprland animates a resize and the terminal reflows to every step of it, so
|
||||||
|
# wait for the grid to hold still before measuring it.
|
||||||
|
settle_grid() {
|
||||||
|
local current previous="" held=0
|
||||||
|
|
||||||
|
for _ in {1..20}; do
|
||||||
|
current=$(stty size)
|
||||||
|
if [[ $current == $previous ]]; then
|
||||||
|
(( ++held == 3 )) && break
|
||||||
|
else
|
||||||
|
held=0
|
||||||
|
previous=$current
|
||||||
|
fi
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
fit_window() {
|
fit_window() {
|
||||||
# A user-level fastfetch config can relocate or restyle the logo in ways this
|
custom_fastfetch_config && return 0
|
||||||
# measurement cannot see, so leave sizing to the float rule in that case.
|
|
||||||
[[ -f $HOME/.config/fastfetch/config.jsonc ]] && return 0
|
|
||||||
|
|
||||||
local rows cols address width height
|
|
||||||
read -r rows cols <<<"$(stty size)"
|
|
||||||
read -r address width height <<<"$(hyprctl clients -j | jq -r '.[] | select(.class == "org.omarchy.about") | "\(.address) \(.size[0]) \(.size[1])"')"
|
|
||||||
[[ -f $LOGO_FILE && -n ${address:-} ]] || return 1
|
|
||||||
(( cols > 0 && rows > 0 && width > 0 && height > 0 )) || return 1
|
|
||||||
|
|
||||||
# Mirror the logo block in the fastfetch config: 2 columns of padding on the
|
|
||||||
# left of the logo, 6 between the logo and the modules, 2 rows above the logo.
|
|
||||||
# wc -L measures display columns, so double-width glyphs count correctly.
|
|
||||||
local logo_w logo_h
|
local logo_w logo_h
|
||||||
logo_w=$(wc -L <"$LOGO_FILE")
|
read -r logo_w logo_h <<<"$(logo_dimensions)"
|
||||||
logo_h=$(( $(wc -l <"$LOGO_FILE") + 2 ))
|
[[ -n ${logo_w:-} ]] || return 1
|
||||||
|
|
||||||
# The guard character keeps command substitution from eating the trailing
|
# The guard character keeps command substitution from eating the trailing
|
||||||
# break line, which provides the bottom padding row.
|
# break line, which provides the bottom padding row.
|
||||||
local modules module_w module_h
|
local modules module_w module_h
|
||||||
modules=$(fastfetch --logo none | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g'; printf X)
|
modules=$(fastfetch --logo none | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g'; printf X)
|
||||||
modules=${modules%X}
|
modules=${modules%X}
|
||||||
module_w=$(printf '%s' "$modules" | wc -L)
|
module_w=$(printf '%s' "$modules" | display_columns)
|
||||||
module_h=$(printf '%s' "$modules" | wc -l)
|
module_h=$(printf '%s' "$modules" | wc -l)
|
||||||
|
|
||||||
# Two columns of right padding to match the logo's left padding, and one row
|
# Mirror the logo block in the fastfetch config: 2 columns of padding left of
|
||||||
# for the cursor line so the trailing break stays visible as bottom padding.
|
# the logo, 6 between logo and modules, 2 rows above it. Then 2 columns of
|
||||||
|
# right padding to match, and a row for the cursor so the trailing break shows.
|
||||||
local target_c=$(( 2 + logo_w + 6 + module_w + 2 ))
|
local target_c=$(( 2 + logo_w + 6 + module_w + 2 ))
|
||||||
local target_r=$(( (logo_h > module_h ? logo_h : module_h) + 1 ))
|
local target_r=$(( (logo_h + 2 > module_h ? logo_h + 2 : module_h) + 1 ))
|
||||||
local target_w=$(( (target_c * width + cols / 2) / cols ))
|
|
||||||
local target_h=$(( (target_r * height + rows / 2) / rows ))
|
|
||||||
|
|
||||||
# Skip resizes within one terminal cell so the fit doesn't churn.
|
local nudges=0 rows cols address width height shift_w shift_h target_w target_h
|
||||||
(( (target_w - width) ** 2 <= (width / cols) ** 2 && (target_h - height) ** 2 <= (height / rows) ** 2 )) && return
|
while :; do
|
||||||
|
read -r rows cols <<<"$(stty size)"
|
||||||
|
read -r address width height <<<"$(hyprctl clients -j | jq -r '.[] | select(.class == "org.omarchy.about") | "\(.address) \(.size[0]) \(.size[1])"')"
|
||||||
|
[[ -n ${address:-} ]] || return 1
|
||||||
|
(( cols > 0 && rows > 0 && width > 0 && height > 0 )) || return 1
|
||||||
|
|
||||||
hypr_dispatch "hl.dsp.window.resize({ window = \"address:$address\", x = $target_w, y = $target_h })" resizewindowpixel "exact $target_w $target_h,address:$address"
|
# A window has to land on the terminal's cell boundaries, so take a cell of
|
||||||
hypr_dispatch "hl.dsp.window.center({ window = \"address:$address\" })" centerwindow
|
# slack over chasing an exact grid, and remember where it came to rest.
|
||||||
|
if (( cols >= target_c && cols <= target_c + 1 && rows >= target_r && rows <= target_r + 1 )); then
|
||||||
|
remember_fit "$logo_w" "$logo_h" "$width" "$height"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Two nudges is the budget, and each one is measured before the next is spent.
|
||||||
|
(( ++nudges <= 2 )) || return 1
|
||||||
|
|
||||||
|
# Move by the cells the window is off by, rather than scaling it to the grid,
|
||||||
|
# which would multiply up the terminal's padding along with them. Dividing a
|
||||||
|
# window that carries that padding still leaves the cell a touch generous, so
|
||||||
|
# round the move away from the grid that would clip.
|
||||||
|
shift_w=$(( (target_c - cols) * width ))
|
||||||
|
shift_h=$(( (target_r - rows) * height ))
|
||||||
|
target_w=$(( width + (shift_w >= 0 ? (shift_w + cols - 1) / cols : shift_w / cols) ))
|
||||||
|
target_h=$(( height + (shift_h >= 0 ? (shift_h + rows - 1) / rows : shift_h / rows) ))
|
||||||
|
|
||||||
|
hypr_dispatch "hl.dsp.window.resize({ window = \"address:$address\", x = $target_w, y = $target_h })" resizewindowpixel "exact $target_w $target_h,address:$address"
|
||||||
|
hypr_dispatch "hl.dsp.window.center({ window = \"address:$address\" })" centerwindow
|
||||||
|
settle_grid
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ ${1:-} == "--render" ]]; then
|
if [[ ${1:-} == "--render" ]]; then
|
||||||
printf '\e[?25l'
|
printf '\e[?25l'
|
||||||
|
|
||||||
# Give the compositor a moment to apply the float rule before measuring cells.
|
# Give the compositor a moment to apply the window rules before measuring cells.
|
||||||
previous=""
|
settle_grid
|
||||||
for _ in {1..20}; do
|
|
||||||
current=$(stty size)
|
|
||||||
[[ $current == $previous ]] && break
|
|
||||||
previous=$current
|
|
||||||
sleep 0.05
|
|
||||||
done
|
|
||||||
|
|
||||||
fitted=false
|
fitted=false
|
||||||
|
passes=0
|
||||||
while :; do
|
while :; do
|
||||||
size=$(stty size)
|
size=$(stty size)
|
||||||
logo_stamp=$(stat -c %Y "$LOGO_FILE" 2>/dev/null)
|
logo_stamp=$(stat -c %Y "$LOGO_FILE" 2>/dev/null)
|
||||||
clear
|
clear
|
||||||
fastfetch
|
fastfetch
|
||||||
if [[ $fitted == false ]] && fit_window; then
|
# A second pass picks up a fit that could not measure the window the first
|
||||||
fitted=true
|
# time. Beyond that, a window that will not settle would be fitted again on
|
||||||
|
# every repaint.
|
||||||
|
if [[ $fitted == false ]] && (( ++passes <= 2 )); then
|
||||||
|
fit_window && fitted=true
|
||||||
fi
|
fi
|
||||||
while [[ $(stty size) == $size && $(stat -c %Y "$LOGO_FILE" 2>/dev/null) == $logo_stamp ]]; do
|
while [[ $(stty size) == $size && $(stat -c %Y "$LOGO_FILE" 2>/dev/null) == $logo_stamp ]]; do
|
||||||
read -t 0.5 -n 1 -s && exit
|
read -t 0.5 -n 1 -s && exit
|
||||||
(( $? > 128 )) || exit
|
(( $? > 128 )) || exit
|
||||||
done
|
done
|
||||||
# A rebranded logo changes the content dimensions, so measure again.
|
# A rebranded logo changes the content dimensions, so measure again.
|
||||||
[[ $(stat -c %Y "$LOGO_FILE" 2>/dev/null) == $logo_stamp ]] || fitted=false
|
if [[ $(stat -c %Y "$LOGO_FILE" 2>/dev/null) != $logo_stamp ]]; then
|
||||||
|
fitted=false
|
||||||
|
passes=0
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
presize_window
|
||||||
exec omarchy-launch-or-focus-tui --app-id=org.omarchy.about omarchy-launch-about --render
|
exec omarchy-launch-or-focus-tui --app-id=org.omarchy.about omarchy-launch-about --render
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Launch the default coding agent in a terminal
|
|
||||||
# omarchy:args=[--inline] [prompt...]
|
|
||||||
# omarchy:examples=omarchy launch agent | omarchy launch agent "Review this project"
|
|
||||||
|
|
||||||
if [[ ${1:-} == "--inline" ]]; then
|
|
||||||
inline=true
|
|
||||||
shift
|
|
||||||
else
|
|
||||||
inline=false
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Agents refuse to remember trust for $HOME, so launches from the keybinding or
|
|
||||||
# menu start in the work directory instead of re-asking on every session.
|
|
||||||
[[ $PWD == "$HOME" && -d $HOME/Work ]] && cd "$HOME/Work"
|
|
||||||
|
|
||||||
agent=$(omarchy-default-agent)
|
|
||||||
|
|
||||||
if omarchy-cmd-missing "$agent"; then
|
|
||||||
echo "$agent is not installed. Choose an installed agent with: omarchy default agent <name>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if (($# > 0)); then
|
|
||||||
prompt="$*"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$agent" in
|
|
||||||
opencode)
|
|
||||||
if [[ -n ${prompt:-} ]]; then
|
|
||||||
command=(opencode --prompt "$prompt")
|
|
||||||
else
|
|
||||||
command=(opencode)
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
gemini)
|
|
||||||
if [[ -n ${prompt:-} ]]; then
|
|
||||||
command=(gemini --prompt-interactive "$prompt")
|
|
||||||
else
|
|
||||||
command=(gemini)
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
copilot)
|
|
||||||
if [[ -n ${prompt:-} ]]; then
|
|
||||||
command=(copilot --interactive "$prompt")
|
|
||||||
else
|
|
||||||
command=(copilot)
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
crush)
|
|
||||||
if [[ -n ${prompt:-} ]]; then
|
|
||||||
command=(crush run "$prompt")
|
|
||||||
else
|
|
||||||
command=(crush)
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
pi | omp | claude | codex | grok)
|
|
||||||
command=("$agent")
|
|
||||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported default agent: $agent" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ $inline == "true" ]]; then
|
|
||||||
exec "${command[@]}"
|
|
||||||
else
|
|
||||||
exec omarchy-launch-tui "${command[@]}"
|
|
||||||
fi
|
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Open the Docker TUI (lazydocker) with access to the Docker daemon
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
# By default the install user is NOT in the docker group: membership is
|
||||||
|
# root-equivalent (a container can bind-mount / and rewrite the host as root),
|
||||||
|
# so a single process running as the user could otherwise escalate to root with
|
||||||
|
# no prompt. lazydocker needs the root-owned Docker socket, so when the group is
|
||||||
|
# absent, gate that access behind a polkit prompt. If the user has opted into
|
||||||
|
# sudoless Docker (omarchy-setup-security-sudoless-docker), the socket is already
|
||||||
|
# reachable, so run lazydocker directly — omarchy-sudo-docker answers that for
|
||||||
|
# this session, so the prompt stays until the reboot that grants the group.
|
||||||
|
# pkexec sanitizes the environment, so carry TERM through for the TUI to render
|
||||||
|
# and run lazydocker from root's PATH.
|
||||||
|
if omarchy-sudo-docker; then
|
||||||
|
exec pkexec /usr/bin/env TERM="${TERM:-xterm-256color}" lazydocker
|
||||||
|
else
|
||||||
|
exec lazydocker
|
||||||
|
fi
|
||||||
@@ -9,8 +9,14 @@
|
|||||||
#
|
#
|
||||||
# Backgrounded because bash defers a trap until a foreground command returns but
|
# Backgrounded because bash defers a trap until a foreground command returns but
|
||||||
# interrupts wait. systemd-cat execs, so the job is Quickshell itself.
|
# interrupts wait. systemd-cat execs, so the job is Quickshell itself.
|
||||||
|
#
|
||||||
|
# Quickshell's own reloading is off; Omarchy restarts the shell deliberately.
|
||||||
|
# A package upgrade rewriting $OMARCHY_PATH/shell would otherwise reload it
|
||||||
|
# against a half-written tree, and that failed reload leaves a second engine
|
||||||
|
# generation behind that turns the next restart's IPC kill into a crash.
|
||||||
run_shell() {
|
run_shell() {
|
||||||
systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" &
|
QS_DISABLE_FILE_WATCHER=1 QS_NO_RELOAD_POPUP=1 \
|
||||||
|
systemd-cat -t omarchy-shell -- quickshell -n -p "$OMARCHY_PATH/shell" &
|
||||||
shell_pid=$!
|
shell_pid=$!
|
||||||
|
|
||||||
local status
|
local status
|
||||||
|
|||||||
+43
-22
@@ -111,8 +111,8 @@ cache_key=$(printf '%s' "$image_dirs_env" | md5sum | cut -d ' ' -f 1)
|
|||||||
rows_cache_file="$cache_dir/$cache_key.rows"
|
rows_cache_file="$cache_dir/$cache_key.rows"
|
||||||
rows_signature_file="$cache_dir/$cache_key.signature"
|
rows_signature_file="$cache_dir/$cache_key.signature"
|
||||||
rows_fast_signature_file="$cache_dir/$cache_key.fast-signature"
|
rows_fast_signature_file="$cache_dir/$cache_key.fast-signature"
|
||||||
rows_signature="v2"$'\n'
|
rows_signature="v3"$'\n'
|
||||||
rows_fast_signature="v1"$'\n'
|
rows_fast_signature="v2"$'\n'
|
||||||
rows_cacheable=true
|
rows_cacheable=true
|
||||||
rows_cache_hit=false
|
rows_cache_hit=false
|
||||||
image_files=()
|
image_files=()
|
||||||
@@ -142,23 +142,32 @@ generate_thumbnail() {
|
|||||||
local image="$1"
|
local image="$1"
|
||||||
local thumbnail="$2"
|
local thumbnail="$2"
|
||||||
local lock="$thumbnail.lock"
|
local lock="$thumbnail.lock"
|
||||||
|
local lock_fd
|
||||||
local tmp="$thumbnail.$$.jpg"
|
local tmp="$thumbnail.$$.jpg"
|
||||||
|
|
||||||
if mkdir "$lock" 2>/dev/null; then
|
# Older releases used directories as locks, which could survive a killed
|
||||||
# Callers fan out one generator per image, so keep each vips single-threaded.
|
# generator and block this thumbnail forever. Only reap aged ones, so a
|
||||||
if VIPS_CONCURRENCY=1 vipsthumbnail "$image" --size 1536x864 --smartcrop=centre --path "$tmp[Q=82,strip]"; then
|
# legacy generator still running through an upgrade keeps its lock.
|
||||||
mv -f "$tmp" "$thumbnail"
|
if [[ -d $lock ]] && (( $(date +%s) - $(stat -c '%Y' "$lock" 2>/dev/null || date +%s) > 120 )); then
|
||||||
else
|
rmdir "$lock" 2>/dev/null
|
||||||
rm -f "$tmp" "$thumbnail"
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
rmdir "$lock" 2>/dev/null || true
|
exec {lock_fd}>"$lock" || return
|
||||||
|
flock -w 30 "$lock_fd" || return
|
||||||
|
|
||||||
|
# A generator killed mid-write leaves its partial $thumbnail.<pid>.jpg
|
||||||
|
# behind. Only the lock holder writes these, so any found now are stale.
|
||||||
|
rm -f "$thumbnail".*.jpg
|
||||||
|
|
||||||
|
[[ -f $thumbnail ]] && return
|
||||||
|
|
||||||
|
# Callers fan out one generator per image, so keep each vips single-threaded.
|
||||||
|
# Close the lock fd for vips: an orphaned or hung vips must not keep holding
|
||||||
|
# the lock after this shell is killed.
|
||||||
|
if VIPS_CONCURRENCY=1 vipsthumbnail "$image" --size 1536x864 --smartcrop=centre --path "$tmp[Q=82,strip]" {lock_fd}>&-; then
|
||||||
|
mv -f "$tmp" "$thumbnail"
|
||||||
else
|
else
|
||||||
for ((i = 0; i < 3000; i++)); do
|
rm -f "$tmp" "$thumbnail"
|
||||||
[[ -f $thumbnail ]] && return
|
|
||||||
[[ -d $lock ]] || break
|
|
||||||
sleep 0.01
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +235,10 @@ elif [[ $rows_cache_hit != true ]]; then
|
|||||||
if [[ -s $pending_file ]]; then
|
if [[ -s $pending_file ]]; then
|
||||||
pruned=""
|
pruned=""
|
||||||
while IFS=$'\t' read -r row_image row_thumbnail; do
|
while IFS=$'\t' read -r row_image row_thumbnail; do
|
||||||
[[ -e $row_thumbnail ]] || continue
|
if [[ ! -e $row_thumbnail ]]; then
|
||||||
|
rows_cacheable=false
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z $pruned ]]; then
|
if [[ -z $pruned ]]; then
|
||||||
pruned="$row_image"$'\t'"$row_thumbnail"
|
pruned="$row_image"$'\t'"$row_thumbnail"
|
||||||
@@ -237,12 +249,21 @@ elif [[ $rows_cache_hit != true ]]; then
|
|||||||
rows="$pruned"
|
rows="$pruned"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $rows_cacheable == true ]]; then
|
# Publish the cache under a lock and via renames: a picker killed mid-write,
|
||||||
printf '%s' "$rows" >"$rows_cache_file"
|
# or two pickers interleaving, must never leave truncated or mismatched rows
|
||||||
printf '%s' "$rows_signature" >"$rows_signature_file"
|
# behind signatures that still validate. Rows go first so a kill between
|
||||||
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file"
|
# renames leaves signatures that are either older (a harmless miss) or
|
||||||
else
|
# describe the same directory state.
|
||||||
rm -f "$rows_cache_file" "$rows_signature_file" "$rows_fast_signature_file"
|
if exec {rows_lock_fd}>"$rows_cache_file.lock" && flock -w 30 "$rows_lock_fd"; then
|
||||||
|
if [[ $rows_cacheable == true ]]; then
|
||||||
|
rm -f "$cache_dir/$cache_key".*.tmp
|
||||||
|
printf '%s' "$rows" >"$rows_cache_file.$$.tmp" && mv -f "$rows_cache_file.$$.tmp" "$rows_cache_file"
|
||||||
|
printf '%s' "$rows_signature" >"$rows_signature_file.$$.tmp" && mv -f "$rows_signature_file.$$.tmp" "$rows_signature_file"
|
||||||
|
printf '%s' "$rows_fast_signature" >"$rows_fast_signature_file.$$.tmp" && mv -f "$rows_fast_signature_file.$$.tmp" "$rows_fast_signature_file"
|
||||||
|
else
|
||||||
|
rm -f "$rows_cache_file" "$rows_signature_file" "$rows_fast_signature_file"
|
||||||
|
fi
|
||||||
|
exec {rows_lock_fd}>&-
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ id=$(cut -f2 <<<"$selection")
|
|||||||
|
|
||||||
if [[ $1 == "clone" ]]; then
|
if [[ $1 == "clone" ]]; then
|
||||||
omarchy-launch-floating-terminal-with-presentation \
|
omarchy-launch-floating-terminal-with-presentation \
|
||||||
"omarchy-plugin-clone --edit $(printf '%q' "$id")"
|
"omarchy-plugin-clone $(printf '%q' "$id") --edit"
|
||||||
elif [[ $1 == "remove" ]]; then
|
elif [[ $1 == "remove" ]]; then
|
||||||
omarchy-launch-floating-terminal-with-presentation "omarchy-plugin-remove $(printf '%q' "$id")"
|
omarchy-launch-floating-terminal-with-presentation "omarchy-plugin-remove $(printf '%q' "$id")"
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ fi
|
|||||||
# The shell keeps the click command with the toast, so this oneshot can hand the
|
# The shell keeps the click command with the toast, so this oneshot can hand the
|
||||||
# invitation over and exit instead of staying activated until it is answered.
|
# invitation over and exit instead of staying activated until it is answered.
|
||||||
omarchy-notification-send -u critical -g "Pending Omarchy Migrations" "$message" \
|
omarchy-notification-send -u critical -g "Pending Omarchy Migrations" "$message" \
|
||||||
--exec "omarchy-launch-floating-terminal-with-presentation omarchy-migrate" && exit 0
|
--exec omarchy-launch-floating-terminal-with-presentation omarchy-migrate && exit 0
|
||||||
|
|
||||||
# Reached when the notification could not be handed off, so fall back to telling
|
# Reached when the notification could not be handed off, so fall back to telling
|
||||||
# the user in the terminal.
|
# the user in the terminal.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ rm -f "$HOME/.local/bin/$command"
|
|||||||
cat >"$HOME/.local/bin/$command" <<EOF
|
cat >"$HOME/.local/bin/$command" <<EOF
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
export MISE_MINIMUM_RELEASE_AGE=0
|
export MISE_MINIMUM_RELEASE_AGE=0
|
||||||
mise use -g "$package" || exit 1
|
mise use -g --quiet "$package" || exit 1
|
||||||
exec mise x "$package" -- "$bin" "\$@"
|
exec mise x "$package" -- "$bin" "\$@"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
+151
-63
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Send an Omarchy desktop notification
|
# omarchy:summary=Send an Omarchy desktop notification
|
||||||
# omarchy:args=[--exec <command>] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]
|
# omarchy:args=[--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [-i <icon>] [-t <ms>] [-r <id>] [-p] [--image <path-or-uri>] <headline> [description] [--exec <program> [args...]]
|
||||||
# omarchy:examples=omarchy notification send "Reminder" "5 minutes are up" -g
|
# omarchy:examples=omarchy notification send "Reminder" "5 minutes are up" -g
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -11,61 +11,75 @@ description=""
|
|||||||
glyph=
|
glyph=
|
||||||
urgency="low"
|
urgency="low"
|
||||||
app_name="omarchy-action"
|
app_name="omarchy-action"
|
||||||
|
app_icon=""
|
||||||
image=
|
image=
|
||||||
exec_command=
|
expire_timeout=-1
|
||||||
args=()
|
replaces_id=0
|
||||||
|
print_id=0
|
||||||
|
exec_args=()
|
||||||
|
exec_present=0
|
||||||
parsed_option_args=0
|
parsed_option_args=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: omarchy-notification-send [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [-i <icon>] [-t <ms>] [-r <id>] [-p] [--image <path-or-uri>] <headline> [description] [--exec <program> [args...]]" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Recognize a known option, in both `--flag value` and `--flag=value` forms.
|
||||||
|
# Returns 1 for anything unrecognized so the caller can decide (headline, or a
|
||||||
|
# hard error in option position).
|
||||||
parse_omarchy_option() {
|
parse_omarchy_option() {
|
||||||
case $1 in
|
local opt val nargs
|
||||||
-g | --glyph)
|
if [[ $1 == --?*=* ]]; then
|
||||||
if (($# < 2)); then
|
opt=${1%%=*}
|
||||||
echo "Missing value for $1" >&2
|
val=${1#*=}
|
||||||
exit 1
|
nargs=1
|
||||||
fi
|
else
|
||||||
glyph=$2
|
opt=$1
|
||||||
parsed_option_args=2
|
val=${2-}
|
||||||
|
nargs=2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -p/--print-id is a flag; it takes no value.
|
||||||
|
if [[ $opt == -p || $opt == --print-id ]]; then
|
||||||
|
print_id=1
|
||||||
|
parsed_option_args=1
|
||||||
return 0
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case $opt in
|
||||||
|
-g | --glyph | -u | --urgency | --app-name | -i | --icon | --image | -r | --replace-id | -t | --expire-time) ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if ((nargs == 2)) && (($# < 2)); then
|
||||||
|
echo "Missing value for $opt" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case $opt in
|
||||||
|
-g | --glyph) glyph=$val ;;
|
||||||
|
-u | --urgency) urgency=$val ;;
|
||||||
|
--app-name) app_name=$val ;;
|
||||||
|
-i | --icon) app_icon=$val ;;
|
||||||
|
--image) image=$val ;;
|
||||||
|
-r | --replace-id)
|
||||||
|
[[ $val =~ ^[0-9]+$ ]] || {
|
||||||
|
echo "Invalid $opt value (numeric id expected): $val" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
replaces_id=$val
|
||||||
;;
|
;;
|
||||||
-u | --urgency)
|
-t | --expire-time)
|
||||||
if (($# < 2)); then
|
[[ $val =~ ^-?[0-9]+$ ]] || {
|
||||||
echo "Missing value for $1" >&2
|
echo "Invalid $opt value (milliseconds expected): $val" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
}
|
||||||
urgency="$2"
|
expire_timeout=$val
|
||||||
parsed_option_args=2
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--app-name)
|
|
||||||
if (($# < 2)); then
|
|
||||||
echo "Missing value for $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
app_name=$2
|
|
||||||
parsed_option_args=2
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--image)
|
|
||||||
if (($# < 2)); then
|
|
||||||
echo "Missing value for $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
image=$2
|
|
||||||
parsed_option_args=2
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
--exec)
|
|
||||||
if (($# < 2)); then
|
|
||||||
echo "Missing value for $1" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exec_command=$2
|
|
||||||
parsed_option_args=2
|
|
||||||
return 0
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
return 1
|
parsed_option_args=$nargs
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
while (($# > 0)); do
|
while (($# > 0)); do
|
||||||
@@ -77,47 +91,121 @@ while (($# > 0)); do
|
|||||||
done
|
done
|
||||||
|
|
||||||
if (($# < 1)); then
|
if (($# < 1)); then
|
||||||
echo "Usage: omarchy-notification-send [--exec <command>] [--app-name <app-name>] [-g <glyph>] [-u <low|normal|critical>] [--image <path-or-uri>] <headline> [description] [notify-send options]"
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
headline=$1
|
headline=$1
|
||||||
shift
|
shift
|
||||||
|
|
||||||
if (($# > 0)) && [[ $1 != -* ]]; then
|
# The description is the next positional, taken as text even when it begins with
|
||||||
|
# a dash — a body like "-50% off" or a negative number is content, not options.
|
||||||
|
# Only a recognized option flag or --exec in that slot is not the description.
|
||||||
|
known_flag() {
|
||||||
|
case $1 in
|
||||||
|
-g | --glyph | -u | --urgency | --app-name | -i | --icon | -t | --expire-time | --image | -r | --replace-id | -p | --print-id | --exec) return 0 ;;
|
||||||
|
--glyph=* | --urgency=* | --app-name=* | --icon=* | --expire-time=* | --image=* | --replace-id=*) return 0 ;;
|
||||||
|
esac
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($# > 0)) && ! known_flag "$1"; then
|
||||||
description=$1
|
description=$1
|
||||||
shift
|
shift
|
||||||
fi
|
fi
|
||||||
|
|
||||||
while (($# > 0)); do
|
while (($# > 0)); do
|
||||||
if parse_omarchy_option "$@"; then
|
if [[ $1 == "--exec" ]]; then
|
||||||
|
# --exec consumes the rest of the line as the click command's argv. The
|
||||||
|
# caller's shell already tokenized those words into discrete arguments, and
|
||||||
|
# the shell runs them as-is (never re-parsed), so untrusted data in an
|
||||||
|
# argument is only ever one argument and can never become a command.
|
||||||
|
# Detected only here, after the headline/description positionals are
|
||||||
|
# captured, so an untrusted headline that is literally "--exec" is taken as
|
||||||
|
# text and can't be mistaken for the delimiter. --exec therefore comes last.
|
||||||
|
shift
|
||||||
|
exec_args=("$@")
|
||||||
|
exec_present=1
|
||||||
|
break
|
||||||
|
elif parse_omarchy_option "$@"; then
|
||||||
shift "$parsed_option_args"
|
shift "$parsed_option_args"
|
||||||
else
|
else
|
||||||
args+=("$1")
|
echo "Unknown option: $1" >&2
|
||||||
shift
|
usage
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Tag as a user-action toast so it pops through DND.
|
case $urgency in
|
||||||
args+=("-a" "$app_name" "-u" "$urgency")
|
low) urgency_byte=0 ;;
|
||||||
|
normal) urgency_byte=1 ;;
|
||||||
|
critical) urgency_byte=2 ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown urgency: $urgency (use low, normal, or critical)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# a{sv} hints, as busctl triples (key, variant type, value). urgency is a byte;
|
||||||
|
# the rest are strings. The click command rides here as omarchy-exec-argv, built
|
||||||
|
# only from --exec below.
|
||||||
|
hints=(urgency y "$urgency_byte")
|
||||||
|
|
||||||
if [[ -n $glyph ]]; then
|
if [[ -n $glyph ]]; then
|
||||||
args+=("--hint=string:omarchy-glyph:$glyph")
|
hints+=(omarchy-glyph s "$glyph")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n $image ]]; then
|
if [[ -n $image ]]; then
|
||||||
args+=("--hint=string:image-path:$image")
|
hints+=(image-path s "$image")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The shell runs the click command itself, from a copy it keeps alongside the
|
if ((exec_present)); then
|
||||||
# on-screen popup. A libnotify action would instead keep this process blocked
|
if ((${#exec_args[@]} == 0)) || [[ -z ${exec_args[0]} ]]; then
|
||||||
# until the click, and die unanswered whenever the shell restarts underneath it.
|
echo "--exec needs a command: --exec <program> [args...]" >&2
|
||||||
if [[ -n $exec_command ]]; then
|
exit 1
|
||||||
args+=("--hint=string:omarchy-exec:$exec_command")
|
fi
|
||||||
|
# A single word with a space is almost always a whole command passed as one
|
||||||
|
# quoted string — which would run a program literally named that. Splitting it
|
||||||
|
# ourselves is exactly the injection we avoid, so reject it and point at the
|
||||||
|
# unquoted form instead.
|
||||||
|
if ((${#exec_args[@]} == 1)) && [[ ${exec_args[0]} == *[[:space:]]* ]]; then
|
||||||
|
echo "--exec takes the command as separate words, not one quoted string." >&2
|
||||||
|
echo "Write: --exec ${exec_args[0]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# NUL-delimit into jq so every byte survives as data: jq's own --args would eat
|
||||||
|
# a bare "--", and a newline in an arg must not split the vector.
|
||||||
|
exec_argv_json=$(printf '%s\0' "${exec_args[@]}" | jq -Rsc 'split("\u0000")[:-1]')
|
||||||
|
hints+=(omarchy-exec-argv s "$exec_argv_json")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n $description ]]; then
|
hint_count=$((${#hints[@]} / 3))
|
||||||
notify-send "${args[@]}" "$headline" "$description"
|
|
||||||
|
# Call org.freedesktop.Notifications.Notify directly — never notify-send. Its
|
||||||
|
# argv parsing is the surface that reinterprets a relayed headline like
|
||||||
|
# `--hint=…` or `-rf` as options or hints; busctl takes each value as one typed
|
||||||
|
# D-Bus parameter instead, and the leading `--` keeps a dash-leading value
|
||||||
|
# (headline, description, a negative timeout) positional rather than a busctl
|
||||||
|
# option. So the summary and body are strings that can never become a hint, and
|
||||||
|
# omarchy-exec-argv is set only from --exec.
|
||||||
|
#
|
||||||
|
# Signature susssasa{sv}i: app_name, replaces_id, app_icon, summary, body,
|
||||||
|
# actions (empty), hints, expire_timeout. replaces_id (from -r) updates a toast
|
||||||
|
# in place; -p prints the returned id so a caller can reuse it.
|
||||||
|
notify_cmd=(
|
||||||
|
busctl --user -- call
|
||||||
|
org.freedesktop.Notifications /org/freedesktop/Notifications
|
||||||
|
org.freedesktop.Notifications Notify susssasa{sv}i
|
||||||
|
"$app_name" "$replaces_id" "$app_icon" "$headline" "$description"
|
||||||
|
0
|
||||||
|
"$hint_count" "${hints[@]}"
|
||||||
|
"$expire_timeout"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ((print_id)); then
|
||||||
|
# busctl prints the UINT32 return as "u <id>"; emit just the id.
|
||||||
|
out=$("${notify_cmd[@]}")
|
||||||
|
printf '%s\n' "${out##* }"
|
||||||
else
|
else
|
||||||
notify-send "${args[@]}" "$headline"
|
"${notify_cmd[@]}" >/dev/null
|
||||||
fi
|
fi
|
||||||
@@ -93,6 +93,13 @@ if [[ -z $url ]]; then
|
|||||||
[[ -n $url ]] || fail "a git URL is required"
|
[[ -n $url ]] || fail "a git URL is required"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Refuse a URL that names a git option or a transport helper before cloning, so
|
||||||
|
# an untrusted URL cannot run a command before the plugin is validated or
|
||||||
|
# enabled. The check is shared with omarchy-theme-install and explains itself; a
|
||||||
|
# missing checker leaves this non-zero, which refuses the URL rather than
|
||||||
|
# cloning it.
|
||||||
|
omarchy-git-url-check "$url" || exit 1
|
||||||
|
|
||||||
if (( ! ASSUME_YES )); then
|
if (( ! ASSUME_YES )); then
|
||||||
cat >&2 <<WARN
|
cat >&2 <<WARN
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ if [[ ! -f $logo_path ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# omarchy-plymouth-set-by-theme passes a theme's unlock.png straight from
|
||||||
|
# ~/.config/omarchy/themes, where an installed theme can make it a symlink to
|
||||||
|
# anything. The copies below land in world-readable /usr/share, so following one
|
||||||
|
# would republish whatever it points at.
|
||||||
|
if [[ -L $logo_path ]]; then
|
||||||
|
echo "Logo file is a symlink, which is not accepted: $logo_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
|
bg_r=$(awk -v n=$((16#${bg_hex:0:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||||
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
|
bg_g=$(awk -v n=$((16#${bg_hex:2:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||||
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
|
bg_b=$(awk -v n=$((16#${bg_hex:4:2})) 'BEGIN{printf "%.3f", n/255}')
|
||||||
@@ -72,7 +81,7 @@ sed \
|
|||||||
-e "s/#ffffff/#$text_hex/g" \
|
-e "s/#ffffff/#$text_hex/g" \
|
||||||
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
"$sddm_template" | sudo tee "$sddm_dir/Main.qml" >/dev/null
|
||||||
|
|
||||||
sudo cp "$logo_path" "$sddm_dir/logo.png"
|
sudo cp "$staging_dir/logo.png" "$sddm_dir/logo.png"
|
||||||
for asset in bullet.png entry.png lock.png; do
|
for asset in bullet.png entry.png lock.png; do
|
||||||
sudo cp "$staging_dir/$asset" "$sddm_dir/$asset"
|
sudo cp "$staging_dir/$asset" "$sddm_dir/$asset"
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ run_first_run_step "install Voxtype post-update hook" \
|
|||||||
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook"
|
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/install-voxtype.hook"
|
||||||
run_first_run_step "install fingerprint setup post-update hook" \
|
run_first_run_step "install fingerprint setup post-update hook" \
|
||||||
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-fingerprint.hook"
|
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-fingerprint.hook"
|
||||||
|
run_first_run_step "install agent setup post-update hook" \
|
||||||
|
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-agent.hook"
|
||||||
|
|
||||||
run_first_run_step "enable user systemd units" \
|
run_first_run_step "enable user systemd units" \
|
||||||
bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh"
|
bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh"
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ say() {
|
|||||||
notice() {
|
notice() {
|
||||||
clear_logo
|
clear_logo
|
||||||
echo
|
echo
|
||||||
gum spin --spinner "pulse" --title "$1" -- sleep "${2:-2}"
|
printf '%*s\033[32m● \033[0m%s\n' "$PADDING_LEFT" '' "$1"
|
||||||
|
sleep "${2:-2}"
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,6 +677,11 @@ user_groups() {
|
|||||||
if [[ -f $PROVISIONING_DIR/groups ]]; then
|
if [[ -f $PROVISIONING_DIR/groups ]]; then
|
||||||
while IFS= read -r group; do
|
while IFS= read -r group; do
|
||||||
[[ -n $group ]] || continue
|
[[ -n $group ]] || continue
|
||||||
|
# Never grant docker at first boot, even if an older install recorded it
|
||||||
|
# (or a factory snapshot predating the opt-in default carries it): the
|
||||||
|
# docker group is root-equivalent. It is opt-in via
|
||||||
|
# omarchy-setup-security-sudoless-docker.
|
||||||
|
[[ $group == "docker" ]] && continue
|
||||||
getent group "$group" >/dev/null || continue
|
getent group "$group" >/dev/null || continue
|
||||||
[[ ",$groups," == *",$group,"* ]] || groups+=",$group"
|
[[ ",$groups," == *",$group,"* ]] || groups+=",$group"
|
||||||
done <"$PROVISIONING_DIR/groups"
|
done <"$PROVISIONING_DIR/groups"
|
||||||
|
|||||||
@@ -83,11 +83,16 @@ fi
|
|||||||
|
|
||||||
# Dev-aware skill symlinks. Cannot live in /etc/skel because OMARCHY_PATH may
|
# Dev-aware skill symlinks. Cannot live in /etc/skel because OMARCHY_PATH may
|
||||||
# point at a dev checkout (omarchy dev link) where the target differs.
|
# point at a dev checkout (omarchy dev link) where the target differs.
|
||||||
|
# Loops every skill directory, so shipping a new one needs no edit here.
|
||||||
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills
|
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills
|
||||||
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.agents/skills/omarchy
|
for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
|
||||||
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.claude/skills/omarchy
|
skill=${skill%/}
|
||||||
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.codex/skills/omarchy
|
name=${skill##*/}
|
||||||
ln -sfn "$OMARCHY_PATH/default/agents/skills/omarchy" ~/.pi/agent/skills/omarchy
|
ln -sfn "$skill" ~/.agents/skills/"$name"
|
||||||
|
ln -sfn "$skill" ~/.claude/skills/"$name"
|
||||||
|
ln -sfn "$skill" ~/.codex/skills/"$name"
|
||||||
|
ln -sfn "$skill" ~/.pi/agent/skills/"$name"
|
||||||
|
done
|
||||||
|
|
||||||
mkdir -p ~/Downloads ~/Pictures ~/Videos ~/.config/gtk-3.0
|
mkdir -p ~/Downloads ~/Pictures ~/Videos ~/.config/gtk-3.0
|
||||||
xdg-user-dirs-update --set TEMPLATES "$HOME"
|
xdg-user-dirs-update --set TEMPLATES "$HOME"
|
||||||
|
|||||||
@@ -20,19 +20,15 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
|
|||||||
omarchy-pkg-drop \
|
omarchy-pkg-drop \
|
||||||
aether \
|
aether \
|
||||||
cliamp \
|
cliamp \
|
||||||
typora \
|
|
||||||
spotify \
|
|
||||||
libreoffice-fresh \
|
libreoffice-fresh \
|
||||||
1password \
|
|
||||||
1password-cli \
|
|
||||||
xournalpp \
|
xournalpp \
|
||||||
signal-desktop \
|
|
||||||
pinta \
|
pinta \
|
||||||
obsidian \
|
obsidian \
|
||||||
obs-studio \
|
obs-studio \
|
||||||
kdenlive \
|
kdenlive \
|
||||||
|
moonlight-qt \
|
||||||
lazydocker \
|
lazydocker \
|
||||||
opencode \
|
omacut \
|
||||||
claude-code \
|
omacalc \
|
||||||
github-cli
|
omawrite
|
||||||
fi
|
fi
|
||||||
@@ -24,9 +24,14 @@ echo -e "\e[32mRemoving FIDO2 device from authentication.\n\e[0m"
|
|||||||
|
|
||||||
remove_pam_config
|
remove_pam_config
|
||||||
|
|
||||||
if [[ -d /etc/fido2 ]]; then
|
authdir=/etc/fido2
|
||||||
|
|
||||||
|
# -d follows symlinks, so a dangling link at /etc/fido2 would survive this and
|
||||||
|
# a later setup would install the authfile through it. rm -rf on a symlink
|
||||||
|
# removes the link itself, never the directory it points at.
|
||||||
|
if [[ -e $authdir || -L $authdir ]]; then
|
||||||
echo "Removing FIDO2 configuration..."
|
echo "Removing FIDO2 configuration..."
|
||||||
sudo rm -rf /etc/fido2
|
sudo rm -rf "$authdir"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Removing FIDO2 packages..."
|
echo "Removing FIDO2 packages..."
|
||||||
|
|||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Disable sudoless Docker by removing your user from the docker group
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Ask about the configured groups, not this session's: right after enabling,
|
||||||
|
# sudoless Docker is on for the account even though the running session still
|
||||||
|
# needs a prompt, and this command is what turns it back off.
|
||||||
|
if omarchy-sudo-docker --configured; then
|
||||||
|
echo "Sudoless Docker is not enabled: $USER is not in the docker group."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Removing $USER from the docker group..."
|
||||||
|
sudo gpasswd -d "$USER" docker >/dev/null
|
||||||
|
|
||||||
|
# Group membership is only re-read by a fresh session, and in practice logging
|
||||||
|
# out or newgrp isn't enough — only a reboot reliably applies it. Record it so a
|
||||||
|
# later `omarchy update` still prompts (omarchy-update-restart reads this), then
|
||||||
|
# offer to do it now.
|
||||||
|
omarchy-state set reboot-required
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Sudoless Docker DISABLED. Docker access goes through a polkit/sudo prompt"
|
||||||
|
echo "again: the Docker TUI (Super + Shift + D) and the Windows VM ask when they"
|
||||||
|
echo "need it, and the plain 'docker' CLI runs under sudo. It takes effect after a reboot."
|
||||||
|
echo ""
|
||||||
|
# The migration reuses this command during 'omarchy update' and defers the
|
||||||
|
# reboot to omarchy-update-restart, so it doesn't cut the update short.
|
||||||
|
if [[ -z ${OMARCHY_DEFER_REBOOT:-} ]] && gum confirm "Reboot now to apply?"; then
|
||||||
|
omarchy-system-reboot
|
||||||
|
fi
|
||||||
@@ -63,7 +63,6 @@ relock_session() {
|
|||||||
|
|
||||||
# Each kill stops the oldest matching instance and only returns once it has
|
# Each kill stops the oldest matching instance and only returns once it has
|
||||||
# fully exited, so the no-duplicate launch below can't race a dying shell.
|
# fully exited, so the no-duplicate launch below can't race a dying shell.
|
||||||
# Requires our quickshell-git build; 0.3.0's kill returns immediately.
|
|
||||||
while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1; do :; done
|
while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1; do :; done
|
||||||
|
|
||||||
# Spawn from Hyprland so the shell inherits the canonical session environment,
|
# Spawn from Hyprland so the shell inherits the canonical session environment,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
|
||||||
check_fido2_hardware() {
|
check_fido2_hardware() {
|
||||||
@@ -50,13 +51,79 @@ if ! check_fido2_hardware; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the pamu2fcfg file
|
# Create the pamu2fcfg file
|
||||||
if [[ ! -f /etc/fido2/fido2 ]]; then
|
authdir=/etc/fido2
|
||||||
sudo mkdir -p /etc/fido2
|
authfile=/etc/fido2/fido2
|
||||||
|
|
||||||
|
# install -d follows a symlink here and applies the mode and ownership to
|
||||||
|
# whatever it points at, so the credential would be staged and published inside
|
||||||
|
# the link target and that directory reopened to root:root 755. This is the
|
||||||
|
# threat omarchy-remove-security-fido2 already names on its side.
|
||||||
|
if [[ -L $authdir || ( -e $authdir && ! -d $authdir ) ]]; then
|
||||||
|
echo -e "\e[31m\n$authdir is not a FIDO2 configuration directory.\e[0m"
|
||||||
|
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -f follows symlinks, so the already-registered check below reads a symlinked
|
||||||
|
# authfile as a registration and leaves it in place, and is false for a
|
||||||
|
# directory, so it tries to register over one. Only a regular file is a valid
|
||||||
|
# pam_u2f authfile.
|
||||||
|
if [[ -L $authfile || ( -e $authfile && ! -f $authfile ) ]]; then
|
||||||
|
echo -e "\e[31m\n$authfile is not a FIDO2 registration file.\e[0m"
|
||||||
|
echo "Run omarchy-remove-security-fido2 first, then set FIDO2 up again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f $authfile ]]; then
|
||||||
|
sudo install -d -m 755 -o root -g root "$authdir"
|
||||||
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
|
echo -e "\e[32m\nLet's setup your device by confirming on the device now.\e[0m"
|
||||||
echo -e "Touch your FIDO2 key when it lights up...\n"
|
echo -e "Touch your FIDO2 key when it lights up...\n"
|
||||||
|
|
||||||
if pamu2fcfg >/tmp/fido2; then
|
# A unique sibling created by root cannot be replaced by another process
|
||||||
sudo mv /tmp/fido2 /etc/fido2/fido2
|
# running as this user. Stream pamu2fcfg into it instead of asking root to
|
||||||
|
# reopen a caller-owned path: an observed temporary name could otherwise be
|
||||||
|
# replaced with a symlink before the privileged copy. The final rename is
|
||||||
|
# atomic, and -T refuses a directory at the destination. Mode 644 keeps the
|
||||||
|
# root-owned global authfile readable when pam_u2f uses openasuser; only root
|
||||||
|
# can still rewrite it.
|
||||||
|
stage=""
|
||||||
|
|
||||||
|
# mktemp's output is an operand for four privileged commands below, one of
|
||||||
|
# them an rm. Take only the name this script asked for rather than whatever
|
||||||
|
# came back on stdout.
|
||||||
|
safe_stage_path() {
|
||||||
|
local candidate=$1
|
||||||
|
local prefix="$authfile.new."
|
||||||
|
local suffix
|
||||||
|
|
||||||
|
[[ $candidate == "$prefix"* ]] || return 1
|
||||||
|
suffix=${candidate#"$prefix"}
|
||||||
|
[[ $suffix =~ ^[[:alnum:]]{6}$ ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_stage() {
|
||||||
|
local status=$?
|
||||||
|
|
||||||
|
if safe_stage_path "$stage"; then
|
||||||
|
sudo rm -f -- "$stage" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
return "$status"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup_stage EXIT
|
||||||
|
stage=$(sudo mktemp "$authfile.new.XXXXXX")
|
||||||
|
|
||||||
|
if ! safe_stage_path "$stage" || [[ ! -f $stage || -L $stage ]]; then
|
||||||
|
echo -e "\e[31m\nCould not create a safe staging file beside $authfile.\e[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if pamu2fcfg | sudo tee "$stage" >/dev/null && [[ -s $stage ]]; then
|
||||||
|
sudo chmod 644 "$stage"
|
||||||
|
sudo mv -Tf "$stage" "$authfile"
|
||||||
|
stage=""
|
||||||
|
trap - EXIT
|
||||||
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
|
echo -e "\e[32mFIDO2 device registered successfully!\e[0m"
|
||||||
else
|
else
|
||||||
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
|
echo -e "\e[31m\nFIDO2 registration failed. Please try again.\e[0m"
|
||||||
|
|||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Enable sudoless Docker by adding your user to the docker group (root-equivalent!)
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Ask about the configured groups, not this session's: once enabled it stays
|
||||||
|
# enabled for the account even before the reboot that lets this session use it.
|
||||||
|
if ! omarchy-sudo-docker --configured; then
|
||||||
|
echo "Sudoless Docker is already enabled: $USER is in the docker group."
|
||||||
|
echo "To disable it again, run: omarchy-remove-security-sudoless-docker"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ WARNING: Enabling sudoless Docker adds you to the 'docker' group."
|
||||||
|
echo ""
|
||||||
|
echo "The Docker daemon runs as root, so membership in the docker group is"
|
||||||
|
echo "equivalent to passwordless root. Any process running as your user could"
|
||||||
|
echo "then run, for example:"
|
||||||
|
echo ""
|
||||||
|
echo " docker run -v /:/host alpine # full read/write of the host, as root"
|
||||||
|
echo ""
|
||||||
|
echo "and take over the machine with no password prompt. A single rogue script,"
|
||||||
|
echo "dependency, or plugin running as you is enough. It is convenient for"
|
||||||
|
echo "development, but it removes the protection Omarchy keeps by default, where"
|
||||||
|
echo "Docker access goes through a polkit/sudo prompt."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if gum confirm "Enable sudoless Docker? This gives anything running as you passwordless root."; then
|
||||||
|
sudo usermod -aG docker "$USER"
|
||||||
|
# A new docker group membership is only picked up by a fresh session, and in
|
||||||
|
# practice logging out or newgrp isn't enough — only a reboot reliably applies
|
||||||
|
# it. Record it so a later `omarchy update` still prompts
|
||||||
|
# (omarchy-update-restart reads this), then offer to do it now.
|
||||||
|
omarchy-state set reboot-required
|
||||||
|
echo ""
|
||||||
|
echo "Sudoless Docker ENABLED. It takes effect after a reboot."
|
||||||
|
echo "To disable it again: Setup > Security > Sudoless Docker."
|
||||||
|
echo ""
|
||||||
|
# The migration reuses this command during 'omarchy update' and defers the
|
||||||
|
# reboot to omarchy-update-restart, so it doesn't cut the update short.
|
||||||
|
if [[ -z ${OMARCHY_DEFER_REBOOT:-} ]] && gum confirm "Reboot now to apply?"; then
|
||||||
|
omarchy-system-reboot
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Aborted. No changes made. Docker access still goes through a prompt."
|
||||||
|
fi
|
||||||
+15
-3
@@ -1,6 +1,18 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Display a "Done!" message with a spinner and wait for user to press any key.
|
# omarchy:summary=Display a "Done!" message and wait for user to press any key.
|
||||||
|
|
||||||
echo
|
# The device node is there whether or not a terminal is behind it, so opening
|
||||||
gum spin --spinner "globe" --title "Done! Press any key to close..." -- bash -c 'read -n 1 -s'
|
# it is the only test that means anything.
|
||||||
|
: 2>/dev/null <>/dev/tty || exit 0
|
||||||
|
|
||||||
|
# gum 2.0 no longer lets a spun command read the terminal, so the wait has to
|
||||||
|
# happen here. Its own query replies are still queued on the tty; drop those or
|
||||||
|
# they answer the keypress for the user.
|
||||||
|
while read -rsn 1 -t 0.1 _ </dev/tty; do :; done
|
||||||
|
|
||||||
|
# Prompt on the terminal rather than stdout, or a caller that redirects us
|
||||||
|
# leaves the user waiting on a prompt they were never shown.
|
||||||
|
printf '\n\033[32m● \033[0mDone! Press any key to close...' >/dev/tty
|
||||||
|
read -rsn 1 </dev/tty
|
||||||
|
echo >/dev/tty
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Succeed when Docker needs sudo, fail when it can be used directly
|
||||||
|
# omarchy:args=[--configured]
|
||||||
|
# omarchy:examples=omarchy-sudo-docker && echo "needs sudo" | omarchy-sudo-docker --configured
|
||||||
|
# omarchy:hidden=true
|
||||||
|
|
||||||
|
# The docker group is root-equivalent, so Omarchy leaves users out of it by
|
||||||
|
# default and reaches the daemon through a prompt instead. Everything that has
|
||||||
|
# to make that choice asks here rather than testing group membership itself.
|
||||||
|
#
|
||||||
|
# Two questions, because they have different answers between toggling sudoless
|
||||||
|
# Docker and the reboot that applies it (group membership is fixed when the
|
||||||
|
# session is created):
|
||||||
|
#
|
||||||
|
# (default) Does Docker need sudo *right now*? Answered by whether this
|
||||||
|
# process can actually reach the socket, which is what decides
|
||||||
|
# if a command must elevate. Still true in the window after
|
||||||
|
# sudoless Docker is enabled but before the reboot.
|
||||||
|
# --configured Will it need sudo once the account's groups take effect?
|
||||||
|
# Answered from the account's configured groups, so the menu
|
||||||
|
# offers the toggle that can actually change state.
|
||||||
|
#
|
||||||
|
# Succeeds (exit 0) when sudo is needed, so it reads as `if omarchy-sudo-docker`.
|
||||||
|
|
||||||
|
DOCKER_SOCKET="${OMARCHY_DOCKER_SOCKET:-/var/run/docker.sock}"
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
--configured)
|
||||||
|
# An account in the docker group will not need sudo after the next login.
|
||||||
|
id -nG "$USER" 2>/dev/null | grep -qw docker && exit 1
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"")
|
||||||
|
# A socket we can write is a daemon we can drive without elevating. A missing
|
||||||
|
# socket counts as needing sudo: reaching it means starting it as root anyway.
|
||||||
|
[[ -w $DOCKER_SOCKET ]] && exit 1
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: omarchy-sudo-docker [--configured]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# omarchy:summary=Reset the sudo lockout/faillock for the current user.
|
|
||||||
|
|
||||||
su -c "faillock --reset --user $USER"
|
|
||||||
@@ -20,4 +20,7 @@ if pgrep -x "1password" >/dev/null && omarchy-cmd-present 1password; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Avoid running screensaver when locked
|
# Avoid running screensaver when locked
|
||||||
|
pkill -x ttfx 2>/dev/null || true
|
||||||
|
# ttfx handles SIGTERM asynchronously, so keep its terminal alive until it exits.
|
||||||
|
timeout 1s pidwait -x ttfx 2>/dev/null || true
|
||||||
pkill -f '[o]rg.omarchy.screensaver' 2>/dev/null || true
|
pkill -f '[o]rg.omarchy.screensaver' 2>/dev/null || true
|
||||||
@@ -59,7 +59,7 @@ claim_path() {
|
|||||||
announce() {
|
announce() {
|
||||||
local path="$1"
|
local path="$1"
|
||||||
local name="${path##*/}"
|
local name="${path##*/}"
|
||||||
local args=("Received $name" "Saved to ${dir/#$HOME/~}")
|
local args=("Received $name" "Saved to ${dir/#$HOME/~}" -u critical)
|
||||||
|
|
||||||
case "${name,,}" in
|
case "${name,,}" in
|
||||||
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
*.png | *.jpg | *.jpeg | *.gif | *.webp | *.avif | *.bmp | *.tif | *.tiff)
|
||||||
@@ -72,7 +72,7 @@ announce() {
|
|||||||
|
|
||||||
# Announcing is best-effort: the file is already delivered, and under `set -e`
|
# Announcing is best-effort: the file is already delivered, and under `set -e`
|
||||||
# a notification outage would otherwise kill the long-running receiver.
|
# a notification outage would otherwise kill the long-running receiver.
|
||||||
omarchy-notification-send "${args[@]}" --exec "$(printf 'xdg-open %q' "$path")" || true
|
omarchy-notification-send "${args[@]}" --exec xdg-open "$path" || true
|
||||||
}
|
}
|
||||||
|
|
||||||
deliver() {
|
deliver() {
|
||||||
|
|||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=List the user-installed themes that came from a git clone
|
||||||
|
# omarchy:examples=omarchy theme extras
|
||||||
|
|
||||||
|
# Exits nonzero when there are none, so a caller can ask whether any exist
|
||||||
|
# without reading the list. A symlinked theme is someone's working copy and a
|
||||||
|
# `.git` file is a worktree pointing elsewhere; neither is ours to pull.
|
||||||
|
status=1
|
||||||
|
|
||||||
|
for theme in ~/.config/omarchy/themes/*; do
|
||||||
|
[[ ! -L $theme && -d $theme/.git ]] || continue
|
||||||
|
echo "$theme"
|
||||||
|
status=0
|
||||||
|
done
|
||||||
|
|
||||||
|
exit $status
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# omarchy:examples=omarchy theme install git@github.com:example/omarchy-example-theme.git
|
# omarchy:examples=omarchy theme install git@github.com:example/omarchy-example-theme.git
|
||||||
|
|
||||||
if [[ -z $1 ]]; then
|
if [[ -z $1 ]]; then
|
||||||
echo -e "\e[32mSee https://manuals.omamix.org/2/the-omarchy-manual/90/extra-themes\n\e[0m"
|
echo -e "\e[32mSee https://omarchy.org/themes/\n\e[0m"
|
||||||
REPO_URL=$(gum input --placeholder="Git repo URL (https or git@host:org/repo.git)" --header="")
|
REPO_URL=$(gum input --placeholder="Git repo URL (https or git@host:org/repo.git)" --header="")
|
||||||
else
|
else
|
||||||
REPO_URL="$1"
|
REPO_URL="$1"
|
||||||
@@ -16,24 +16,38 @@ if [[ -z $REPO_URL ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Refuse a URL that names a git option or a transport helper before cloning. The
|
||||||
|
# check is shared with omarchy-plugin-add and explains itself; a missing checker
|
||||||
|
# leaves this non-zero, which refuses the URL rather than cloning it.
|
||||||
|
omarchy-git-url-check "$REPO_URL" || exit 1
|
||||||
|
|
||||||
THEMES_DIR="$HOME/.config/omarchy/themes"
|
THEMES_DIR="$HOME/.config/omarchy/themes"
|
||||||
|
|
||||||
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path
|
# Strip user@host: prefix from scp-style SSH URLs so basename sees just the path
|
||||||
REPO_PATH="$REPO_URL"
|
REPO_PATH="$REPO_URL"
|
||||||
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:*/* ]] && REPO_PATH="${REPO_PATH#*:}"
|
[[ $REPO_PATH != *"://"* && $REPO_PATH == *:*/* ]] && REPO_PATH="${REPO_PATH#*:}"
|
||||||
THEME_NAME=$(basename "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
|
THEME_NAME=$(basename -- "$REPO_PATH" .git | sed -E 's/^omarchy-//; s/-theme$//' | tr '[:upper:]' '[:lower:]')
|
||||||
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
||||||
|
|
||||||
|
# The name comes from the URL and is joined into a path that is about to be
|
||||||
|
# removed, so a repo called `..` would take ~/.config/omarchy with it. A leading
|
||||||
|
# dot is refused with it: `host:-s/foo.git` leaves basename with `.git`.
|
||||||
|
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||||
|
echo "Error: '$REPO_URL' does not give a usable theme name."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove existing theme if present
|
# Remove existing theme if present
|
||||||
if [[ -d $THEME_PATH ]]; then
|
if [[ -d $THEME_PATH ]]; then
|
||||||
rm -rf "$THEME_PATH"
|
rm -rf "$THEME_PATH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clone the repo directly to ~/.config/omarchy/themes
|
# Clone the repo directly to ~/.config/omarchy/themes
|
||||||
if ! git clone "$REPO_URL" "$THEME_PATH"; then
|
if ! git clone -- "$REPO_URL" "$THEME_PATH"; then
|
||||||
echo "Error: Failed to clone theme repo."
|
echo "Error: Failed to clone theme repo."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Apply the new theme with omarchy-theme-set
|
# Apply the new theme with omarchy-theme-set, which stages only the files an
|
||||||
omarchy-theme-set $THEME_NAME
|
# extra theme is allowed to contribute and names anything it dropped.
|
||||||
|
omarchy-theme-set "$THEME_NAME"
|
||||||
@@ -21,8 +21,9 @@ fi
|
|||||||
THEMES_DIR="$HOME/.config/omarchy/themes"
|
THEMES_DIR="$HOME/.config/omarchy/themes"
|
||||||
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
THEME_PATH="$THEMES_DIR/$THEME_NAME"
|
||||||
|
|
||||||
# Ensure a theme was set
|
# Ensure a theme was set, and that the name cannot climb out of THEMES_DIR
|
||||||
if [[ -z $THEME_NAME ]]; then
|
# on its way into the rm below.
|
||||||
|
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+130
-2
@@ -17,6 +17,19 @@ THEME_SET_LOCK="${XDG_RUNTIME_DIR:-/tmp}/omarchy-theme-set.lock"
|
|||||||
USER_THEMES_PATH="$HOME/.config/omarchy/themes"
|
USER_THEMES_PATH="$HOME/.config/omarchy/themes"
|
||||||
OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes"
|
OMARCHY_THEMES_PATH="$OMARCHY_PATH/themes"
|
||||||
|
|
||||||
|
# What a theme installed from a git repo may not ship, because these run code.
|
||||||
|
# Hyprland requires a theme's hyprland.lua and gum_env.lua at login and Neovim
|
||||||
|
# loads its neovim.lua at startup, so no .lua from such a theme is staged at all.
|
||||||
|
# Each terminal config names the program the terminal launches, and vscode.json
|
||||||
|
# names an extension omarchy-theme-set-vscode installs, a VS Code extension being
|
||||||
|
# arbitrary JavaScript. Everything else a theme ships is colour and is kept.
|
||||||
|
#
|
||||||
|
# Adding a template for another terminal, or for another editor that loads Lua,
|
||||||
|
# means adding it here. test/shell.d/theme-staging-test.sh fails on a generated
|
||||||
|
# theme file that is neither denied here nor recorded there as colour-only.
|
||||||
|
INSTALLED_THEME_DENIED=(alacritty.toml foot.ini ghostty.conf kitty.conf vscode.json)
|
||||||
|
IGNORED_THEME_FILES=()
|
||||||
|
|
||||||
run_parallel() {
|
run_parallel() {
|
||||||
local pid
|
local pid
|
||||||
local pids=()
|
local pids=()
|
||||||
@@ -122,12 +135,121 @@ set_theme_background() {
|
|||||||
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
|
ln -nsf "$new_background" "$CURRENT_BACKGROUND_LINK"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_denied_installed_file() {
|
||||||
|
local name="$1"
|
||||||
|
local denied
|
||||||
|
|
||||||
|
[[ $name == *.lua ]] && return 0
|
||||||
|
|
||||||
|
for denied in "${INSTALLED_THEME_DENIED[@]}"; do
|
||||||
|
[[ $name == "$denied" ]] && return 0
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Themes older than colors.toml still get their palette, but their
|
||||||
|
# alacritty.toml never reaches the staged theme: an Alacritty config names the
|
||||||
|
# program the terminal launches.
|
||||||
|
stage_installed_colors_from_alacritty() {
|
||||||
|
local source="$1"
|
||||||
|
local scratch
|
||||||
|
|
||||||
|
if [[ -f $NEXT_THEME_PATH/colors.toml ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f $source/alacritty.toml || -L $source/alacritty.toml ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
scratch=$(mktemp -d)
|
||||||
|
cp "$source/alacritty.toml" "$scratch/alacritty.toml"
|
||||||
|
omarchy-theme-colors-from-alacritty "$scratch"
|
||||||
|
|
||||||
|
if [[ -f $scratch/colors.toml ]]; then
|
||||||
|
cp "$scratch/colors.toml" "$NEXT_THEME_PATH/colors.toml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$scratch"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copies a directory without ever following a symlink: in an installed theme one
|
||||||
|
# points wherever the theme author chose, which is how an unlock.png becomes a
|
||||||
|
# copy of any file the session can read.
|
||||||
|
stage_installed_dir() {
|
||||||
|
local source="$1"
|
||||||
|
local dest="$2"
|
||||||
|
local entry name
|
||||||
|
|
||||||
|
mkdir -p "$dest"
|
||||||
|
|
||||||
|
for entry in "$source"/*; do
|
||||||
|
[[ -e $entry && ! -L $entry ]] || continue
|
||||||
|
name=${entry##*/}
|
||||||
|
|
||||||
|
if [[ -d $entry ]]; then
|
||||||
|
stage_installed_dir "$entry" "$dest/$name"
|
||||||
|
else
|
||||||
|
cp "$entry" "$dest/$name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# `omarchy theme install` clones into ~/.config/omarchy/themes, so a .git
|
||||||
|
# directory there means the contents came from a stranger and are held to the
|
||||||
|
# list above. A directory the user wrote themselves, and a symlink to their own
|
||||||
|
# working copy, are theirs to fill however they like -- the same distinction
|
||||||
|
# omarchy-theme-extras draws when it decides which themes it may pull.
|
||||||
|
theme_came_from_a_repo() {
|
||||||
|
local source="$1"
|
||||||
|
|
||||||
|
[[ ! -L $source && -d $source/.git ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
stage_installed_theme() {
|
||||||
|
local source="$1"
|
||||||
|
local entry name
|
||||||
|
|
||||||
|
[[ -d $source ]] || return 0
|
||||||
|
|
||||||
|
for entry in "$source"/*; do
|
||||||
|
[[ -e $entry ]] || continue
|
||||||
|
name=${entry##*/}
|
||||||
|
|
||||||
|
if [[ -L $entry ]] || is_denied_installed_file "$name"; then
|
||||||
|
case "${name,,}" in
|
||||||
|
readme* | license* | changelog* | *.md | *.txt) ;;
|
||||||
|
*) IGNORED_THEME_FILES+=("$name") ;;
|
||||||
|
esac
|
||||||
|
elif [[ -d $entry ]]; then
|
||||||
|
stage_installed_dir "$entry" "$NEXT_THEME_PATH/$name"
|
||||||
|
else
|
||||||
|
cp "$entry" "$NEXT_THEME_PATH/$name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
stage_installed_colors_from_alacritty "$source"
|
||||||
|
}
|
||||||
|
|
||||||
|
report_ignored_theme_files() {
|
||||||
|
(( ${#IGNORED_THEME_FILES[@]} > 0 )) || return 0
|
||||||
|
|
||||||
|
echo "Ignored in $USER_THEMES_PATH/$THEME_NAME: ${IGNORED_THEME_FILES[*]}" >&2
|
||||||
|
echo "A theme installed from a git repo cannot supply Lua, a terminal config, or vscode.json." >&2
|
||||||
|
}
|
||||||
|
|
||||||
THEME_NAME=$(echo "$1" | sed -E 's/<[^>]+>//g' | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
THEME_NAME=$(echo "$1" | sed -E 's/<[^>]+>//g' | tr '[:upper:]' '[:lower:]' | tr ' ' '-')
|
||||||
THEME_HEADLESS=0
|
THEME_HEADLESS=0
|
||||||
if [[ ${OMARCHY_THEME_HEADLESS:-} == "1" || ${OMARCHY_THEME_OFFLINE:-} == "1" ]]; then
|
if [[ ${OMARCHY_THEME_HEADLESS:-} == "1" || ${OMARCHY_THEME_OFFLINE:-} == "1" ]]; then
|
||||||
THEME_HEADLESS=1
|
THEME_HEADLESS=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -z $THEME_NAME || $THEME_NAME == .* || $THEME_NAME == */* ]]; then
|
||||||
|
echo "Invalid theme name: $1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! -d $OMARCHY_THEMES_PATH/$THEME_NAME ]] && [[ ! -d $USER_THEMES_PATH/$THEME_NAME ]]; then
|
if [[ ! -d $OMARCHY_THEMES_PATH/$THEME_NAME ]] && [[ ! -d $USER_THEMES_PATH/$THEME_NAME ]]; then
|
||||||
echo "Theme '$THEME_NAME' does not exist"
|
echo "Theme '$THEME_NAME' does not exist"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -143,9 +265,15 @@ flock 9
|
|||||||
rm -rf "$NEXT_THEME_PATH"
|
rm -rf "$NEXT_THEME_PATH"
|
||||||
mkdir -p "$NEXT_THEME_PATH"
|
mkdir -p "$NEXT_THEME_PATH"
|
||||||
|
|
||||||
# Copy official theme first, then overlay user customizations on top
|
# Copy official theme first, then overlay the user's theme on top
|
||||||
cp -r "$OMARCHY_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
|
cp -r "$OMARCHY_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
|
||||||
cp -r "$USER_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
|
|
||||||
|
if theme_came_from_a_repo "$USER_THEMES_PATH/$THEME_NAME"; then
|
||||||
|
stage_installed_theme "$USER_THEMES_PATH/$THEME_NAME"
|
||||||
|
report_ignored_theme_files
|
||||||
|
else
|
||||||
|
cp -r "$USER_THEMES_PATH/$THEME_NAME/"* "$NEXT_THEME_PATH/" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
# Generate colors.toml from alacritty.toml if theme is missing colors.toml
|
# Generate colors.toml from alacritty.toml if theme is missing colors.toml
|
||||||
if [[ ! -f $NEXT_THEME_PATH/colors.toml && -f $NEXT_THEME_PATH/alacritty.toml ]]; then
|
if [[ ! -f $NEXT_THEME_PATH/colors.toml && -f $NEXT_THEME_PATH/alacritty.toml ]]; then
|
||||||
|
|||||||
@@ -150,4 +150,4 @@ set_theme() {
|
|||||||
! omarchy-toggle-enabled skip-vscode-theme-changes && set_theme "code" "$HOME/.config/Code/User/settings.json" "$HOME/.vscode/extensions"
|
! omarchy-toggle-enabled skip-vscode-theme-changes && set_theme "code" "$HOME/.config/Code/User/settings.json" "$HOME/.vscode/extensions"
|
||||||
! omarchy-toggle-enabled skip-vscode-insiders-theme-changes && set_theme "code-insiders" "$HOME/.config/Code - Insiders/User/settings.json" "$HOME/.vscode-insiders/extensions"
|
! omarchy-toggle-enabled skip-vscode-insiders-theme-changes && set_theme "code-insiders" "$HOME/.config/Code - Insiders/User/settings.json" "$HOME/.vscode-insiders/extensions"
|
||||||
! omarchy-toggle-enabled skip-codium-theme-changes && set_theme "codium" "$HOME/.config/VSCodium/User/settings.json" "$HOME/.vscode-oss/extensions"
|
! omarchy-toggle-enabled skip-codium-theme-changes && set_theme "codium" "$HOME/.config/VSCodium/User/settings.json" "$HOME/.vscode-oss/extensions"
|
||||||
! omarchy-toggle-enabled skip-cursor-theme-changes && set_theme "cursor" "$HOME/.config/Cursor/User/settings.json" "$HOME/.cursor/extensions"
|
! omarchy-toggle-enabled skip-cursor-theme-changes && set_theme "/usr/bin/cursor" "$HOME/.config/Cursor/User/settings.json" "$HOME/.cursor/extensions"
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
# omarchy:summary=Update user-installed git themes
|
# omarchy:summary=Update user-installed git themes
|
||||||
|
|
||||||
for dir in ~/.config/omarchy/themes/*/; do
|
mapfile -t themes < <(omarchy-theme-extras)
|
||||||
if [[ -d $dir ]] && [[ ! -L ${dir%/} ]] && [[ -d $dir/.git ]]; then
|
|
||||||
echo "Updating: $(basename "$dir")"
|
for theme in "${themes[@]}"; do
|
||||||
git -C "$dir" pull
|
echo "Updating: $(basename "$theme")"
|
||||||
fi
|
git -C "$theme" pull
|
||||||
done
|
done
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Toggle crash capture notifications
|
||||||
|
|
||||||
|
# nf-md-robot_dead, escaped so this file reads without a Nerd Font.
|
||||||
|
readonly CRASH_GLYPH=$'\U000f16a1'
|
||||||
|
|
||||||
|
omarchy-toggle crash-capture-off
|
||||||
|
|
||||||
|
# The unit checks the same flag with ConditionPathExists, so stopping and
|
||||||
|
# starting only settles this session; the next login reads the flag itself.
|
||||||
|
if omarchy-toggle-enabled crash-capture-off; then
|
||||||
|
systemctl --user stop omarchy-crash-watch.service
|
||||||
|
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture disabled"
|
||||||
|
else
|
||||||
|
systemctl --user start omarchy-crash-watch.service
|
||||||
|
omarchy-notification-send -g "$CRASH_GLYPH" "Crash capture enabled"
|
||||||
|
fi
|
||||||
@@ -22,7 +22,20 @@ CONF
|
|||||||
sudo systemctl enable --now supergfxd
|
sudo systemctl enable --now supergfxd
|
||||||
fi
|
fi
|
||||||
|
|
||||||
gpu_mode=$(supergfxctl -g)
|
gpu_mode=""
|
||||||
|
for attempt in {1..3}; do
|
||||||
|
if gpu_mode=$(timeout --kill-after=1s 3s supergfxctl -g 2>/dev/null) && [[ -n $gpu_mode ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
gpu_mode=""
|
||||||
|
(( attempt < 3 )) && sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z $gpu_mode ]]; then
|
||||||
|
echo "supergfxd is not responding. Try again, or check: systemctl status supergfxd" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
case "$gpu_mode" in
|
case "$gpu_mode" in
|
||||||
"Integrated")
|
"Integrated")
|
||||||
|
|||||||
@@ -7,44 +7,70 @@
|
|||||||
KIND="${1:-}"
|
KIND="${1:-}"
|
||||||
ACTION="${2:-toggle}"
|
ACTION="${2:-toggle}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
||||||
|
}
|
||||||
|
|
||||||
case "$KIND" in
|
case "$KIND" in
|
||||||
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
|
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
|
||||||
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
|
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Hyprland sources this directory on reload, so the disabled state survives restarts
|
# The persisted disable is the device name stored as plain data; on every
|
||||||
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled.lua"
|
# reload default/hypr/disabled-input-device.lua reads it back and disables the
|
||||||
|
# device. Names come from USB descriptors and must not be interpolated into
|
||||||
|
# shell or Lua. The path is hardcoded to ~/.local/state like the sibling
|
||||||
|
# toggle tools, so it keeps working when XDG_STATE_HOME diverges.
|
||||||
|
NAME_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled-name"
|
||||||
|
|
||||||
device="$("omarchy-hw-$KIND")"
|
device="$("omarchy-hw-$KIND")"
|
||||||
|
|
||||||
if [[ -z $device ]]; then
|
require_device() {
|
||||||
echo "No $KIND device found" >&2
|
if [[ -z $device ]]; then
|
||||||
exit 1
|
echo "No $KIND device found" >&2
|
||||||
fi
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $device == *[[:cntrl:]]* ]]; then
|
||||||
|
echo "Invalid $KIND device name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_device() {
|
||||||
|
local enabled=$1
|
||||||
|
local quoted=${device//\\/\\\\}
|
||||||
|
quoted=${quoted//\"/\\\"}
|
||||||
|
hyprctl eval "hl.device({ name = \"$quoted\", enabled = $enabled })" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
|
# Clear the persisted state before requiring a usable device, so a device
|
||||||
rm -f "$STATE_FILE"
|
# that stops reporting a valid name can never wedge the disable in place.
|
||||||
|
rm -f "$NAME_FILE"
|
||||||
|
require_device
|
||||||
|
apply_device true
|
||||||
omarchy-osd -i "$ICON" -m "$LABEL enabled"
|
omarchy-osd -i "$ICON" -m "$LABEL enabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
disable() {
|
disable() {
|
||||||
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
|
require_device
|
||||||
mkdir -p "$(dirname "$STATE_FILE")"
|
apply_device false
|
||||||
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
|
mkdir -p "$(dirname "$NAME_FILE")"
|
||||||
|
printf '%s\n' "$device" >"$NAME_FILE"
|
||||||
omarchy-osd -i "$ICON" -m "$LABEL disabled"
|
omarchy-osd -i "$ICON" -m "$LABEL disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$ACTION" in
|
case "$ACTION" in
|
||||||
on) enable ;;
|
on) enable ;;
|
||||||
off) disable ;;
|
off) disable ;;
|
||||||
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
|
toggle) if [[ -f $NAME_FILE ]]; then enable; else disable; fi ;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
|
usage
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -21,7 +21,15 @@ trap 'omarchy-update-stay-awake stop' EXIT
|
|||||||
|
|
||||||
omarchy-update-requires-free-space
|
omarchy-update-requires-free-space
|
||||||
|
|
||||||
|
# -y is a promise not to ask anything. Steps that would need an answer report
|
||||||
|
# and move on instead of waiting on a prompt nobody is here to give.
|
||||||
|
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
|
||||||
|
|
||||||
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
||||||
|
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
|
||||||
|
# after it frees nothing until that snapshot ages out.
|
||||||
|
omarchy-update-pkg-prune
|
||||||
|
|
||||||
# 127 means Snapper is deliberately absent. Any other failure already said
|
# 127 means Snapper is deliberately absent. Any other failure already said
|
||||||
# what went wrong, and a missing snapshot is not worth blocking an update
|
# what went wrong, and a missing snapshot is not worth blocking an update
|
||||||
# over, but it must not pass for one either.
|
# over, but it must not pass for one either.
|
||||||
@@ -32,6 +40,10 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
|
|||||||
|
|
||||||
omarchy-update-dev
|
omarchy-update-dev
|
||||||
omarchy-update-keyring
|
omarchy-update-keyring
|
||||||
|
|
||||||
|
# Migrations ship with the packages installed here and are written against
|
||||||
|
# them, so everything below waits on this finishing. An upgrade that stopped
|
||||||
|
# takes the update with it rather than migrating against what is still on disk.
|
||||||
omarchy-update-system-pkgs
|
omarchy-update-system-pkgs
|
||||||
omarchy-migrate
|
omarchy-migrate
|
||||||
omarchy-hook post-update
|
omarchy-hook post-update
|
||||||
|
|||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# omarchy:summary=Prune superseded versions from the pacman package cache
|
||||||
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# The cache is the only offline downgrade path, so keep two. Run before the
|
||||||
|
# packages update and the installed version survives with a spare. paccache
|
||||||
|
# retains by version order, never by what is installed.
|
||||||
|
echo -e "\e[32m\nPrune package cache\e[0m"
|
||||||
|
sudo paccache -rk2 || echo -e "\e[33mCould not prune the package cache.\e[0m" >&2
|
||||||
|
echo
|
||||||
@@ -5,6 +5,14 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# The conflict handler's last resort: pacman puts its questions to the person
|
||||||
|
# running the update instead of answering them itself. Nothing here may capture
|
||||||
|
# a stream, because an upgrade without --noconfirm prompts on stderr. The
|
||||||
|
# handler has already said why, so no heading here either.
|
||||||
|
if [[ ${OMARCHY_UPDATE_CONFLICT:-} == 1 && ${OMARCHY_UPDATE_INTERACTIVE:-} == 1 ]]; then
|
||||||
|
exec sudo env OMARCHY_UPDATE_PACMAN=1 pacman -Syu --overwrite '/usr/share/omarchy/*'
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e "\e[32m\nUpdate system packages\e[0m"
|
echo -e "\e[32m\nUpdate system packages\e[0m"
|
||||||
|
|
||||||
errors=$(mktemp)
|
errors=$(mktemp)
|
||||||
@@ -22,9 +30,9 @@ if sudo env LC_ALL=C OMARCHY_UPDATE_PACMAN=1 pacman -Syu --noconfirm \
|
|||||||
fi
|
fi
|
||||||
cat "$errors" >&2
|
cat "$errors" >&2
|
||||||
|
|
||||||
# An upgrade blocked only by files pacman doesn't own yet is the one failure
|
# The handler takes it from here: it clears files pacman doesn't own yet and
|
||||||
# worth retrying: the handler clears them and runs this again. Anything else,
|
# runs this again, and puts a package conflict to whoever started the update.
|
||||||
# including a second failure, is for a human.
|
# Anything else, including a second failure, is for a human.
|
||||||
[[ ${OMARCHY_UPDATE_RETRY:-} != 1 ]] || exit 1
|
[[ ${OMARCHY_UPDATE_RETRY:-} != 1 ]] || exit 1
|
||||||
# exec, so the EXIT trap above does not fire and the handler can still read the
|
# exec, so the EXIT trap above does not fire and the handler can still read the
|
||||||
# report. It takes over deleting it.
|
# report. It takes over deleting it.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Retry a system package update after resolving file conflicts
|
# omarchy:summary=Retry a system package update that hit a conflict
|
||||||
# omarchy:hidden=true
|
# omarchy:hidden=true
|
||||||
|
|
||||||
# Internal to omarchy-update-system-pkgs. Not a command to run by hand: it acts
|
# Internal to omarchy-update-system-pkgs. Not a command to run by hand: it acts
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
# release's installer, an in-place edit -- belong to nobody, and pacman refuses
|
# release's installer, an in-place edit -- belong to nobody, and pacman refuses
|
||||||
# to install over a file it doesn't own. This clears them and runs the upgrade
|
# to install over a file it doesn't own. This clears them and runs the upgrade
|
||||||
# again.
|
# again.
|
||||||
|
#
|
||||||
|
# Packages that conflict with each other are the other blocked upgrade, and that
|
||||||
|
# one is a decision rather than a cleanup, so it goes back to the person
|
||||||
|
# watching the update.
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -57,6 +61,26 @@ restore_moved() {
|
|||||||
trap 'restore_moved; rm -f "$errors"' EXIT
|
trap 'restore_moved; rm -f "$errors"' EXIT
|
||||||
trap 'exit 1' INT TERM
|
trap 'exit 1' INT TERM
|
||||||
|
|
||||||
|
# Pacman answers its own conflict question with No under --noconfirm, so one
|
||||||
|
# retired package can stop every upgrade after it. Which package to drop is a
|
||||||
|
# decision -- an upstream split retires one, but so does a package the user
|
||||||
|
# picked on purpose -- so put the question to whoever started the update rather
|
||||||
|
# than guess at it. Pacman printed what conflicts just above.
|
||||||
|
if grep -q 'unresolvable package conflicts detected' "$errors"; then
|
||||||
|
# -y promised not to ask anything, and without a terminal there is nowhere to
|
||||||
|
# ask; either way pacman would sit on a prompt nobody answers. An upgrade that
|
||||||
|
# isn't running --noconfirm puts its questions on stderr and reads the answers
|
||||||
|
# from stdin, so those are the two that have to be a terminal.
|
||||||
|
if [[ ${OMARCHY_UPDATE_UNATTENDED:-} == 1 || ! -t 0 || ! -t 2 ]]; then
|
||||||
|
echo -e "\e[33m\nThis upgrade needs an answer. Run omarchy update interactively to give it.\e[0m" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\e[33m\nA package conflict stopped this upgrade. Running it again so you can answer:\e[0m"
|
||||||
|
OMARCHY_UPDATE_INTERACTIVE=1 omarchy-update-system-pkgs
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Paths one of these packages installs that pacman doesn't own. Moving them out
|
# Paths one of these packages installs that pacman doesn't own. Moving them out
|
||||||
# of the way is what lets the upgrade through, and works on a leftover directory
|
# of the way is what lets the upgrade through, and works on a leftover directory
|
||||||
# as well as a file.
|
# as well as a file.
|
||||||
|
|||||||
@@ -1317,9 +1317,9 @@ apply_system_transition() {
|
|||||||
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
printf '%s\n' '{"browser":{"theme":{"color_scheme":0,"color_scheme2":0}}}' | \
|
||||||
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
|
as_root tee /usr/lib/chromium/initial_preferences >/dev/null
|
||||||
|
|
||||||
if getent group docker >/dev/null; then
|
# Deliberately do NOT add the user to the docker group. That group is
|
||||||
as_root usermod -aG docker "$target_user"
|
# root-equivalent, so it is opt-in now (Setup > Security > Sudoless Docker);
|
||||||
fi
|
# re-granting it on upgrade would silently undo that default for everyone.
|
||||||
|
|
||||||
if [[ -f /usr/bin/powerprofilesctl ]]; then
|
if [[ -f /usr/bin/powerprofilesctl ]]; then
|
||||||
as_root sed -i '/env python3/ c\#!/bin/python3' /usr/bin/powerprofilesctl || true
|
as_root sed -i '/env python3/ c\#!/bin/python3' /usr/bin/powerprofilesctl || true
|
||||||
@@ -1792,82 +1792,6 @@ CHROMIUM_FLAGS_PATCH_PY
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# True if any Chromium-family browser that stores the Copy URL shortcut is running.
|
|
||||||
browser_is_running() {
|
|
||||||
pgrep -x 'chromium|chrome|brave|msedge|vivaldi-bin|vivaldi|opera|helium' >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
repair_chromium_copy_url_shortcuts() {
|
|
||||||
local profile_root preferences
|
|
||||||
local -a profile_roots=(
|
|
||||||
"$HOME/.config/chromium"
|
|
||||||
"$HOME/.config/google-chrome"
|
|
||||||
"$HOME/.config/google-chrome-beta"
|
|
||||||
"$HOME/.config/BraveSoftware/Brave-Browser"
|
|
||||||
"$HOME/.config/BraveSoftware/Brave-Browser-Beta"
|
|
||||||
"$HOME/.config/microsoft-edge"
|
|
||||||
"$HOME/.config/microsoft-edge-beta"
|
|
||||||
"$HOME/.config/vivaldi"
|
|
||||||
"$HOME/.config/opera"
|
|
||||||
"$HOME/.config/helium"
|
|
||||||
)
|
|
||||||
local -a pending=()
|
|
||||||
|
|
||||||
for profile_root in "${profile_roots[@]}"; do
|
|
||||||
[[ -d $profile_root ]] || continue
|
|
||||||
|
|
||||||
for preferences in "$profile_root"/*/Preferences; do
|
|
||||||
[[ -f $preferences ]] || continue
|
|
||||||
grep -qF 'bocglpkldciamkbmlphanhkfnhpmnbma' "$preferences" || continue
|
|
||||||
pending+=("$preferences")
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
(( ${#pending[@]} )) || return 0
|
|
||||||
|
|
||||||
# A running browser holds Preferences in memory and rewrites it on exit,
|
|
||||||
# which would revert this repair. Ask the user to close it first.
|
|
||||||
if browser_is_running && (( ! OMARCHY_UPGRADE_YES )) && [[ -r /dev/tty ]]; then
|
|
||||||
if ! gum confirm "Close all browser windows before the Copy URL shortcut is repaired, then continue" </dev/tty; then
|
|
||||||
echo "Skipped Copy URL shortcut repair; re-run after closing your browser." >&2
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
for preferences in "${pending[@]}"; do
|
|
||||||
python3 - "$preferences" "$preferences.omarchy-upgrade-to-quattro.$BACKUP_SUFFIX.bak" <<'CHROMIUM_SHORTCUTS_PATCH_PY'
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
path = Path(sys.argv[1])
|
|
||||||
backup_path = Path(sys.argv[2])
|
|
||||||
old_id = "bocglpkldciamkbmlphanhkfnhpmnbma"
|
|
||||||
new_id = "bgpiichlckmfanooecilcjemknkcpngb"
|
|
||||||
preferences = json.loads(path.read_text())
|
|
||||||
extensions = preferences.get("extensions", {})
|
|
||||||
commands = extensions.get("commands", {})
|
|
||||||
changed = False
|
|
||||||
|
|
||||||
for command in commands.values():
|
|
||||||
if command.get("extension") == old_id and command.get("command_name") == "copy-url":
|
|
||||||
command["extension"] = new_id
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if changed:
|
|
||||||
settings = extensions.get("settings", {})
|
|
||||||
old_command = settings.get(old_id, {}).get("commands", {}).get("copy-url", {})
|
|
||||||
new_command = settings.get(new_id, {}).get("commands", {}).get("copy-url", {})
|
|
||||||
old_command.pop("was_assigned", None)
|
|
||||||
if new_command:
|
|
||||||
new_command["was_assigned"] = True
|
|
||||||
shutil.copy2(path, backup_path)
|
|
||||||
path.write_text(json.dumps(preferences, separators=(",", ":")))
|
|
||||||
CHROMIUM_SHORTCUTS_PATCH_PY
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
repair_sleep_lock_unit_override() {
|
repair_sleep_lock_unit_override() {
|
||||||
local unit="$HOME/.config/systemd/user/omarchy-sleep-lock.service"
|
local unit="$HOME/.config/systemd/user/omarchy-sleep-lock.service"
|
||||||
local legacy_exec='ExecStart=%h/.local/share/omarchy/bin/omarchy-system-sleep-monitor'
|
local legacy_exec='ExecStart=%h/.local/share/omarchy/bin/omarchy-system-sleep-monitor'
|
||||||
@@ -1964,7 +1888,6 @@ copy_missing_config_defaults "$root/config" "$HOME/.config"
|
|||||||
mark_removed_preinstalls_from_legacy_bindings
|
mark_removed_preinstalls_from_legacy_bindings
|
||||||
copy_always_config_defaults
|
copy_always_config_defaults
|
||||||
repair_chromium_copy_url_extension_flags
|
repair_chromium_copy_url_extension_flags
|
||||||
repair_chromium_copy_url_shortcuts
|
|
||||||
|
|
||||||
# Carry over user-added backgrounds from the legacy git checkout backup, then
|
# Carry over user-added backgrounds from the legacy git checkout backup, then
|
||||||
# remove old symlinks that pointed ~/.config/omarchy/themes/* back into the
|
# remove old symlinks that pointed ~/.config/omarchy/themes/* back into the
|
||||||
|
|||||||
+487
-117
@@ -4,11 +4,371 @@
|
|||||||
# omarchy:args=<install|remove|launch|stop|status> [options]
|
# omarchy:args=<install|remove|launch|stop|status> [options]
|
||||||
# omarchy:requires-sudo=true
|
# omarchy:requires-sudo=true
|
||||||
|
|
||||||
COMPOSE_FILE="$HOME/.config/windows/docker-compose.yml"
|
# The Windows VM runs a privileged container (KVM, /dev/net/tun, NET_ADMIN), so
|
||||||
|
# it needs the root-owned Docker daemon. By default the user is NOT in the docker
|
||||||
|
# group (that group is root-equivalent), so Docker access is gated behind a
|
||||||
|
# single polkit prompt. If the user opted into sudoless Docker
|
||||||
|
# (omarchy-setup-security-sudoless-docker), the socket is reachable directly and
|
||||||
|
# no prompt appears.
|
||||||
|
#
|
||||||
|
# The compose file lives in a root-owned directory and is only ever written by
|
||||||
|
# the elevated, input-validated write_compose action below. That is the whole
|
||||||
|
# point: a root-invoked `docker compose up` must never consume a file that a
|
||||||
|
# process running as the user could have rewritten to bind-mount / into the
|
||||||
|
# container. Earlier versions kept it under ~/.config/windows, which a rogue
|
||||||
|
# process could edit and then trigger a privileged bring-up of — a file-swap
|
||||||
|
# path to root. Do not move it back under $HOME.
|
||||||
|
|
||||||
|
RUNTIME_DIR="${OMARCHY_WINDOWS_DIR:-/var/lib/omarchy/windows}"
|
||||||
|
COMPOSE_FILE="$RUNTIME_DIR/docker-compose.yml"
|
||||||
|
LEGACY_COMPOSE_FILE="$HOME/.config/windows/docker-compose.yml"
|
||||||
|
# The guest password lives in the root-owned compose (readable by root and the
|
||||||
|
# docker group), but RDP needs it as the user. Keep a private copy here, 0600 in
|
||||||
|
# the user's own config, so the plaintext password is never world-readable.
|
||||||
|
CREDENTIALS_FILE="$HOME/.config/windows/credentials"
|
||||||
|
IMAGE="dockurr/windows"
|
||||||
|
CONTAINER="omarchy-windows"
|
||||||
|
|
||||||
|
# --- privilege helpers -------------------------------------------------------
|
||||||
|
|
||||||
|
# True when this session can reach the Docker socket directly (sudoless Docker
|
||||||
|
# on and in effect). Asking about the socket rather than the configured groups
|
||||||
|
# keeps the prompt in place through the window where sudoless Docker is enabled
|
||||||
|
# but the reboot that grants the group has not happened yet.
|
||||||
|
docker_needs_sudo() { omarchy-sudo-docker; }
|
||||||
|
|
||||||
|
# The command to hand pkexec for the privileged re-exec. pkexec runs whatever
|
||||||
|
# executable it is given (after authorization) and only shows the path in the
|
||||||
|
# prompt — it does NOT require the target to be root-owned. So resolve to the
|
||||||
|
# packaged command and refuse to elevate anything a non-root user could have
|
||||||
|
# written: a PATH-injected shim, or a user-owned dev checkout. Without this, a
|
||||||
|
# prompt the user grants for the trusted helper could run an attacker's binary
|
||||||
|
# as root. Fails closed (empty output) when no trustworthy target is found.
|
||||||
|
priv_target() {
|
||||||
|
local candidate owner mode
|
||||||
|
for candidate in /usr/bin/omarchy-windows-vm "$(type -P omarchy-windows-vm 2>/dev/null)"; do
|
||||||
|
[[ -n $candidate && -x $candidate ]] || continue
|
||||||
|
owner=$(stat -Lc '%u' "$candidate" 2>/dev/null) || continue
|
||||||
|
mode=$(stat -Lc '%a' "$candidate" 2>/dev/null) || continue
|
||||||
|
[[ $owner == "0" ]] || continue
|
||||||
|
((8#$mode & 022)) && continue # writable by group or other -> reject
|
||||||
|
printf '%s\n' "$candidate"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run a privileged VM action. write_compose always elevates (the compose is
|
||||||
|
# root-owned); the daemon operations run directly when sudoless Docker is on and
|
||||||
|
# otherwise behind a polkit prompt. The stock org.freedesktop.policykit.exec
|
||||||
|
# policy is auth_admin (not auth_admin_keep), so each elevated action prompts:
|
||||||
|
# a launch asks once to start and, unless authorization is still cached by the
|
||||||
|
# agent, again to stop.
|
||||||
|
priv() {
|
||||||
|
local action="$1"
|
||||||
|
shift
|
||||||
|
if [[ $action != write_compose ]] && ! docker_needs_sudo; then
|
||||||
|
"__priv_$action" "$@"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local target
|
||||||
|
target=$(priv_target) || {
|
||||||
|
echo "omarchy-windows-vm: refusing to run a non-root-owned command as root" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
pkexec "$target" __priv "$action" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
dc() { docker-compose -f "$COMPOSE_FILE" "$@"; }
|
||||||
|
|
||||||
|
# --- validation (shared by the user-side prompts and the root-side writer) ----
|
||||||
|
|
||||||
|
valid_ram() { [[ $1 =~ ^[0-9]{1,3}G$ ]]; }
|
||||||
|
valid_cores() { [[ $1 =~ ^[0-9]{1,2}$ ]] && ((10#$1 >= 1)); }
|
||||||
|
valid_disk() { [[ $1 =~ ^[0-9]{1,4}G$ ]]; }
|
||||||
|
valid_username() { [[ $1 =~ ^[A-Za-z0-9_-]{1,20}$ ]]; }
|
||||||
|
valid_tz() { [[ $1 =~ ^[A-Za-z0-9_/.+-]{1,64}$ ]]; }
|
||||||
|
valid_path() {
|
||||||
|
[[ $1 =~ ^/[A-Za-z0-9._/-]+$ ]] || return 1
|
||||||
|
# Reject non-normalized paths: a . or .. component canonicalizes at mount time
|
||||||
|
# (e.g. /./ or /a/../ -> /), which would bind-mount a sensitive directory —
|
||||||
|
# host / included — into the guest. Volumes must be given already-normalized.
|
||||||
|
case "$1" in
|
||||||
|
*//* | */./* | */../* | */. | */..) return 1 ;;
|
||||||
|
esac
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
valid_password() { [[ $1 =~ ^[[:print:]]{1,64}$ ]]; }
|
||||||
|
|
||||||
|
# The only privileged sub-actions __priv may dispatch. A bash command name
|
||||||
|
# containing a slash is executed as a path, so validating the action here — not
|
||||||
|
# just interpolating it into "__priv_${action}" — is what stops an action like
|
||||||
|
# ../tmp/evil from running an arbitrary file as root.
|
||||||
|
valid_priv_action() {
|
||||||
|
case "$1" in
|
||||||
|
write_compose | up | up_wait | down | status | remove) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- privileged actions (run as root via pkexec, or directly when sudoless) ---
|
||||||
|
|
||||||
|
# Reads KEY=VALUE lines on stdin, re-validates every field, and writes the
|
||||||
|
# compose atomically as root. Re-validation here is the security boundary: the
|
||||||
|
# writer refuses rather than emit a compose an attacker could have influenced.
|
||||||
|
# Only these fixed keys are honored; image, container name, devices, caps, and
|
||||||
|
# port bindings are hard-coded and never taken from input.
|
||||||
|
__priv_write_compose() {
|
||||||
|
local ram cores disk username password tz storage shared key value
|
||||||
|
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
case "$key" in
|
||||||
|
RAM) ram="$value" ;;
|
||||||
|
CORES) cores="$value" ;;
|
||||||
|
DISK) disk="$value" ;;
|
||||||
|
USERNAME) username="$value" ;;
|
||||||
|
PASSWORD) password="$value" ;;
|
||||||
|
TZ) tz="$value" ;;
|
||||||
|
STORAGE) storage="$value" ;;
|
||||||
|
SHARED) shared="$value" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
valid_ram "$ram" || { echo "invalid RAM: $ram" >&2; exit 2; }
|
||||||
|
valid_cores "$cores" || { echo "invalid CPU cores: $cores" >&2; exit 2; }
|
||||||
|
valid_disk "$disk" || { echo "invalid disk size: $disk" >&2; exit 2; }
|
||||||
|
valid_username "$username" || { echo "invalid username: $username" >&2; exit 2; }
|
||||||
|
valid_password "$password" || { echo "invalid password" >&2; exit 2; }
|
||||||
|
valid_tz "$tz" || tz="UTC"
|
||||||
|
valid_path "$storage" || { echo "invalid storage path: $storage" >&2; exit 2; }
|
||||||
|
valid_path "$shared" || { echo "invalid shared path: $shared" >&2; exit 2; }
|
||||||
|
|
||||||
|
# Neutralize anything in the password that could be misread when the compose
|
||||||
|
# is parsed. Two layers apply, in this order at parse time: docker compose
|
||||||
|
# variable interpolation over the raw text ($VAR / $$), then YAML parsing of
|
||||||
|
# the double-quoted scalar. Encode for the inner layer first (backslash, then
|
||||||
|
# double-quote) and the interpolation layer last ($ -> $$), so a password
|
||||||
|
# containing " \ or $ reaches the guest verbatim. unescape() reverses this in
|
||||||
|
# the opposite order for the RDP credentials.
|
||||||
|
local esc_password=${password//\\/\\\\}
|
||||||
|
esc_password=${esc_password//\"/\\\"}
|
||||||
|
esc_password=${esc_password//\$/\$\$}
|
||||||
|
|
||||||
|
mkdir -p "$RUNTIME_DIR"
|
||||||
|
chmod 0755 "$RUNTIME_DIR" 2>/dev/null || true
|
||||||
|
chown root:root "$RUNTIME_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp "$RUNTIME_DIR/.compose.XXXXXX")
|
||||||
|
cat >"$tmp" <<EOF
|
||||||
|
services:
|
||||||
|
windows:
|
||||||
|
image: $IMAGE
|
||||||
|
container_name: $CONTAINER
|
||||||
|
environment:
|
||||||
|
VERSION: "11"
|
||||||
|
RAM_SIZE: "$ram"
|
||||||
|
CPU_CORES: "$cores"
|
||||||
|
DISK_SIZE: "$disk"
|
||||||
|
USERNAME: "$username"
|
||||||
|
PASSWORD: "$esc_password"
|
||||||
|
TZ: "$tz"
|
||||||
|
ARGUMENTS: "-rtc base=localtime,clock=host,driftfix=slew"
|
||||||
|
devices:
|
||||||
|
- /dev/kvm
|
||||||
|
- /dev/net/tun
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
ports:
|
||||||
|
- 127.0.0.1:8006:8006
|
||||||
|
- 127.0.0.1:3389:3389/tcp
|
||||||
|
- 127.0.0.1:3389:3389/udp
|
||||||
|
volumes:
|
||||||
|
- $storage:/storage
|
||||||
|
- $shared:/shared
|
||||||
|
restart: "no"
|
||||||
|
stop_grace_period: 2m
|
||||||
|
EOF
|
||||||
|
# Readable by root and the docker group only. In sudoless mode the user is in
|
||||||
|
# the docker group and runs docker-compose against this file directly; in the
|
||||||
|
# default mode the elevated helper (root) reads it, and other local users
|
||||||
|
# cannot read the guest password. chown falls back to root:root if the docker
|
||||||
|
# group somehow does not exist (sudoless mode could not be enabled anyway).
|
||||||
|
chmod 0640 "$tmp" 2>/dev/null || true
|
||||||
|
chown root:docker "$tmp" 2>/dev/null || chown root:root "$tmp" 2>/dev/null || true
|
||||||
|
mv -f "$tmp" "$COMPOSE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the host source of a bind mount out of the compose (e.g. /storage).
|
||||||
|
# valid_path kept a ':' out of the stored path, so splitting on it is safe.
|
||||||
|
get_mount_source() {
|
||||||
|
sed -n "s|^[[:space:]]*-[[:space:]]*\(/[^:]*\):$1\$|\1|p" "$COMPOSE_FILE" | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Refuse to bring the VM up if a bind-mount source is a symlink, or reached
|
||||||
|
# through one. valid_path keeps a traversal string like /./ out of the compose,
|
||||||
|
# but a symlink planted at ~/.windows or ~/Windows would redirect the privileged
|
||||||
|
# mount just the same — docker follows it — and a string check cannot see that.
|
||||||
|
# So verify the real directories here, as root, immediately before the mount. A
|
||||||
|
# source that does not exist is fine: docker creates it as a plain directory.
|
||||||
|
assert_mounts_safe() {
|
||||||
|
local mnt src real
|
||||||
|
for mnt in /storage /shared; do
|
||||||
|
src=$(get_mount_source "$mnt")
|
||||||
|
[[ -n $src ]] || {
|
||||||
|
echo "omarchy-windows-vm: missing $mnt mount source in the compose" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if [[ -L $src ]]; then
|
||||||
|
echo "omarchy-windows-vm: refusing to start — $src is a symlink; the VM mount source must be a real directory" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ -e $src ]]; then
|
||||||
|
[[ -d $src ]] || {
|
||||||
|
echo "omarchy-windows-vm: refusing to start — $src is not a directory" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
real=$(realpath "$src" 2>/dev/null)
|
||||||
|
[[ $real == "$src" ]] || {
|
||||||
|
echo "omarchy-windows-vm: refusing to start — $src resolves through a symlink to $real" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
__priv_up() { assert_mounts_safe && dc up -d; }
|
||||||
|
|
||||||
|
__priv_down() { dc down; }
|
||||||
|
|
||||||
|
# Bring the VM up and wait until the guest reports it is ready, all under a
|
||||||
|
# single elevation so the readiness poll does not prompt on every iteration.
|
||||||
|
__priv_up_wait() {
|
||||||
|
assert_mounts_safe || return 1
|
||||||
|
local status
|
||||||
|
status=$(docker inspect --format='{{.State.Status}}' "$CONTAINER" 2>/dev/null)
|
||||||
|
if [[ $status != "running" ]]; then
|
||||||
|
dc up -d || return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# docker logs persists across restarts, so anchor the scan to the current
|
||||||
|
# start time; an empty --since would match a stale "started successfully".
|
||||||
|
local started_at count=0
|
||||||
|
while true; do
|
||||||
|
started_at=$(docker inspect --format='{{.State.StartedAt}}' "$CONTAINER" 2>/dev/null)
|
||||||
|
if [[ -n $started_at ]] && docker logs --since "$started_at" "$CONTAINER" 2>&1 | grep -qi "windows started successfully"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
((++count > 60)) && {
|
||||||
|
echo "Timeout: Windows VM did not report ready within 2 minutes" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print the status (empty if the container does not exist) and always succeed,
|
||||||
|
# so a non-zero exit from priv status means the elevation itself failed
|
||||||
|
# (authorization declined) rather than "no such container".
|
||||||
|
__priv_status() { docker inspect --format='{{.State.Status}}' "$CONTAINER" 2>/dev/null || true; }
|
||||||
|
|
||||||
|
__priv_remove() {
|
||||||
|
dc down 2>/dev/null || true
|
||||||
|
docker rmi "$IMAGE" 2>/dev/null || true
|
||||||
|
rm -f "$COMPOSE_FILE"
|
||||||
|
rmdir "$RUNTIME_DIR" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- config helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
# Feed the collected settings to the elevated writer.
|
||||||
|
write_compose() {
|
||||||
|
local ram="$1" cores="$2" disk="$3" username="$4" password="$5" tz="$6" storage="$7" shared="$8"
|
||||||
|
printf 'RAM=%s\nCORES=%s\nDISK=%s\nUSERNAME=%s\nPASSWORD=%s\nTZ=%s\nSTORAGE=%s\nSHARED=%s\n' \
|
||||||
|
"$ram" "$cores" "$disk" "$username" "$password" "$tz" "$storage" "$shared" |
|
||||||
|
priv write_compose
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse, in the opposite order, the escaping __priv_write_compose applied to
|
||||||
|
# the password: undo the interpolation layer ($$ -> $) first, then the YAML
|
||||||
|
# layer (\" -> ", then \\ -> \).
|
||||||
|
unescape() {
|
||||||
|
local v=$1
|
||||||
|
v=${v//\$\$/\$}
|
||||||
|
v=${v//\\\"/\"}
|
||||||
|
v=${v//\\\\/\\}
|
||||||
|
printf '%s' "$v"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Store the RDP credentials privately for the user (0600) so the plaintext
|
||||||
|
# password is not world-readable. The password is one validated printable line
|
||||||
|
# (no newline), so plain KEY=VALUE is safe.
|
||||||
|
write_credentials() {
|
||||||
|
local username="$1" password="$2" old_umask
|
||||||
|
mkdir -p "$(dirname "$CREDENTIALS_FILE")"
|
||||||
|
old_umask=$(umask)
|
||||||
|
umask 077
|
||||||
|
printf 'USERNAME=%s\nPASSWORD=%s\n' "$username" "$password" >"$CREDENTIALS_FILE"
|
||||||
|
chmod 600 "$CREDENTIALS_FILE" 2>/dev/null || true
|
||||||
|
umask "$old_umask"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read one field from the private credentials file; IFS on the first = keeps a
|
||||||
|
# password that itself contains =.
|
||||||
|
read_credential() {
|
||||||
|
local want="$1" key value
|
||||||
|
[[ -f $CREDENTIALS_FILE ]] || return 1
|
||||||
|
while IFS='=' read -r key value; do
|
||||||
|
[[ $key == "$want" ]] && {
|
||||||
|
printf '%s' "$value"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
done <"$CREDENTIALS_FILE"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read_compose_value() {
|
||||||
|
local key="$1" file="$2"
|
||||||
|
sed -n "s/.*${key}: \"\(.*\)\"/\1/p" "$file" | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Older installs kept the compose under ~/.config/windows. Carry those settings
|
||||||
|
# into the root-owned location (preserving the VM's data via the same volume
|
||||||
|
# paths) so an upgrade does not strand or re-download an existing VM.
|
||||||
|
migrate_legacy_compose() {
|
||||||
|
[[ -f $COMPOSE_FILE ]] && return 0
|
||||||
|
[[ -f $LEGACY_COMPOSE_FILE ]] || return 1
|
||||||
|
|
||||||
|
echo "Migrating Windows VM configuration to $COMPOSE_FILE ..."
|
||||||
|
local ram cores disk username password tz storage shared
|
||||||
|
ram=$(read_compose_value RAM_SIZE "$LEGACY_COMPOSE_FILE")
|
||||||
|
cores=$(read_compose_value CPU_CORES "$LEGACY_COMPOSE_FILE")
|
||||||
|
disk=$(read_compose_value DISK_SIZE "$LEGACY_COMPOSE_FILE")
|
||||||
|
username=$(read_compose_value USERNAME "$LEGACY_COMPOSE_FILE")
|
||||||
|
password=$(read_compose_value PASSWORD "$LEGACY_COMPOSE_FILE")
|
||||||
|
tz=$(read_compose_value TZ "$LEGACY_COMPOSE_FILE")
|
||||||
|
# The VM's data always lived in the user's own ~/.windows and ~/Windows; the
|
||||||
|
# old compose only ever recorded those. Reconstruct them from $HOME (trusted —
|
||||||
|
# this runs as the user) rather than reading host paths back from a file a
|
||||||
|
# rogue process could have rewritten to bind-mount, say, / into the guest.
|
||||||
|
storage="$HOME/.windows"
|
||||||
|
shared="$HOME/Windows"
|
||||||
|
|
||||||
|
[[ -z $tz ]] && tz="UTC"
|
||||||
|
if ! write_compose "$ram" "$cores" "$disk" "$username" "$password" "$tz" "$storage" "$shared"; then
|
||||||
|
echo "Could not migrate the existing configuration automatically." >&2
|
||||||
|
echo "Re-run: omarchy-windows-vm install" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
write_credentials "$username" "$password"
|
||||||
|
rm -f "$LEGACY_COMPOSE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- prerequisites -----------------------------------------------------------
|
||||||
|
|
||||||
check_prerequisites() {
|
check_prerequisites() {
|
||||||
local DISK_SIZE_GB=${1:-64}
|
local DISK_SIZE_GB=${1:-64}
|
||||||
local REQUIRED_SPACE=$((DISK_SIZE_GB + 10)) # Add 10GB for Windows ISO and overhead
|
local REQUIRED_SPACE=$((DISK_SIZE_GB + 10)) # Add 10GB for Windows ISO and overhead
|
||||||
|
|
||||||
# Check for KVM support
|
# Check for KVM support
|
||||||
if [[ ! -e /dev/kvm ]]; then
|
if [[ ! -e /dev/kvm ]]; then
|
||||||
@@ -26,7 +386,7 @@ check_prerequisites() {
|
|||||||
|
|
||||||
# Check disk space
|
# Check disk space
|
||||||
AVAILABLE_SPACE=$(df "$HOME" | awk 'NR==2 {print int($4/1024/1024)}')
|
AVAILABLE_SPACE=$(df "$HOME" | awk 'NR==2 {print int($4/1024/1024)}')
|
||||||
if (( AVAILABLE_SPACE < REQUIRED_SPACE )); then
|
if ((AVAILABLE_SPACE < REQUIRED_SPACE)); then
|
||||||
echo "❌ Insufficient disk space!"
|
echo "❌ Insufficient disk space!"
|
||||||
echo " Available: ${AVAILABLE_SPACE}GB"
|
echo " Available: ${AVAILABLE_SPACE}GB"
|
||||||
echo " Required: ${REQUIRED_SPACE}GB (${DISK_SIZE_GB}GB disk + 10GB for Windows image)"
|
echo " Required: ${REQUIRED_SPACE}GB (${DISK_SIZE_GB}GB disk + 10GB for Windows image)"
|
||||||
@@ -34,6 +394,8 @@ check_prerequisites() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- commands ----------------------------------------------------------------
|
||||||
|
|
||||||
install_windows() {
|
install_windows() {
|
||||||
# Set up trap to handle Ctrl+C
|
# Set up trap to handle Ctrl+C
|
||||||
trap "echo ''; echo 'Installation cancelled by user'; exit 1" INT
|
trap "echo ''; echo 'Installation cancelled by user'; exit 1" INT
|
||||||
@@ -43,10 +405,9 @@ install_windows() {
|
|||||||
omarchy-pkg-add freerdp openbsd-netcat gum
|
omarchy-pkg-add freerdp openbsd-netcat gum
|
||||||
|
|
||||||
mkdir -p "$HOME/.windows"
|
mkdir -p "$HOME/.windows"
|
||||||
mkdir -p "$HOME/.config/windows"
|
|
||||||
mkdir -p "$HOME/.local/share/applications"
|
mkdir -p "$HOME/.local/share/applications"
|
||||||
|
|
||||||
cat << EOF | tee "$HOME/.local/share/applications/windows-vm.desktop" > /dev/null
|
cat <<EOF | tee "$HOME/.local/share/applications/windows-vm.desktop" >/dev/null
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Windows
|
Name=Windows
|
||||||
Comment=Start Windows VM via Docker and connect with RDP
|
Comment=Start Windows VM via Docker and connect with RDP
|
||||||
@@ -70,7 +431,7 @@ EOF
|
|||||||
|
|
||||||
RAM_OPTIONS=""
|
RAM_OPTIONS=""
|
||||||
for size in 2 4 8 16 32 64; do
|
for size in 2 4 8 16 32 64; do
|
||||||
if (( size <= TOTAL_RAM_GB )); then
|
if ((size <= TOTAL_RAM_GB)); then
|
||||||
RAM_OPTIONS="$RAM_OPTIONS ${size}G"
|
RAM_OPTIONS="$RAM_OPTIONS ${size}G"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -91,16 +452,16 @@ EOF
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! [[ $SELECTED_CORES =~ ^[0-9]+$ ]] || (( SELECTED_CORES < 1 )) || (( SELECTED_CORES > TOTAL_CORES )); then
|
if ! valid_cores "$SELECTED_CORES" || ((SELECTED_CORES > TOTAL_CORES)); then
|
||||||
echo "Invalid input. Using default: 2 cores"
|
echo "Invalid input. Using default: 2 cores"
|
||||||
SELECTED_CORES=2
|
SELECTED_CORES=2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
AVAILABLE_SPACE=$(df "$HOME" | awk 'NR==2 {print int($4/1024/1024)}')
|
AVAILABLE_SPACE=$(df "$HOME" | awk 'NR==2 {print int($4/1024/1024)}')
|
||||||
MAX_DISK_GB=$((AVAILABLE_SPACE - 10)) # Leave 10GB for Windows image
|
MAX_DISK_GB=$((AVAILABLE_SPACE - 10)) # Leave 10GB for Windows image
|
||||||
|
|
||||||
# Check if we have enough space for minimum
|
# Check if we have enough space for minimum
|
||||||
if (( MAX_DISK_GB < 32 )); then
|
if ((MAX_DISK_GB < 32)); then
|
||||||
echo "❌ Insufficient disk space for Windows VM!"
|
echo "❌ Insufficient disk space for Windows VM!"
|
||||||
echo " Available: ${AVAILABLE_SPACE}GB"
|
echo " Available: ${AVAILABLE_SPACE}GB"
|
||||||
echo " Minimum required: 42GB (32GB disk + 10GB for Windows image)"
|
echo " Minimum required: 42GB (32GB disk + 10GB for Windows image)"
|
||||||
@@ -109,7 +470,7 @@ EOF
|
|||||||
|
|
||||||
DISK_OPTIONS=""
|
DISK_OPTIONS=""
|
||||||
for size in 32 64 128 256 512; do
|
for size in 32 64 128 256 512; do
|
||||||
if (( size <= MAX_DISK_GB )); then
|
if ((size <= MAX_DISK_GB)); then
|
||||||
DISK_OPTIONS="$DISK_OPTIONS ${size}G"
|
DISK_OPTIONS="$DISK_OPTIONS ${size}G"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -139,6 +500,10 @@ EOF
|
|||||||
if [[ -z $USERNAME ]]; then
|
if [[ -z $USERNAME ]]; then
|
||||||
USERNAME="docker"
|
USERNAME="docker"
|
||||||
fi
|
fi
|
||||||
|
if ! valid_username "$USERNAME"; then
|
||||||
|
echo "Invalid username (use letters, digits, - or _, up to 20 chars). Using default: docker"
|
||||||
|
USERNAME="docker"
|
||||||
|
fi
|
||||||
|
|
||||||
PASSWORD=$(gum input --placeholder="Password (Press enter to use default: admin)" --password --header="Enter Windows password:")
|
PASSWORD=$(gum input --placeholder="Password (Press enter to use default: admin)" --password --header="Enter Windows password:")
|
||||||
if [[ -z $PASSWORD ]]; then
|
if [[ -z $PASSWORD ]]; then
|
||||||
@@ -147,6 +512,11 @@ EOF
|
|||||||
else
|
else
|
||||||
PASSWORD_DISPLAY="(user-defined)"
|
PASSWORD_DISPLAY="(user-defined)"
|
||||||
fi
|
fi
|
||||||
|
if ! valid_password "$PASSWORD"; then
|
||||||
|
echo "Invalid password (printable characters, up to 64). Using default: admin"
|
||||||
|
PASSWORD="admin"
|
||||||
|
PASSWORD_DISPLAY="(default)"
|
||||||
|
fi
|
||||||
|
|
||||||
# Display configuration summary
|
# Display configuration summary
|
||||||
gum style \
|
gum style \
|
||||||
@@ -170,38 +540,19 @@ EOF
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p $HOME/Windows
|
mkdir -p "$HOME/Windows"
|
||||||
|
|
||||||
# Create docker-compose.yml in user config directory
|
local tz
|
||||||
cat << EOF | tee "$COMPOSE_FILE" > /dev/null
|
tz=$(timedatectl show -p Timezone --value 2>/dev/null || echo UTC)
|
||||||
services:
|
|
||||||
windows:
|
# Write the root-owned compose from the validated settings (one prompt if
|
||||||
image: dockurr/windows
|
# sudoless Docker is off), then bring the stack up.
|
||||||
container_name: omarchy-windows
|
write_compose "$SELECTED_RAM" "$SELECTED_CORES" "$SELECTED_DISK" \
|
||||||
environment:
|
"$USERNAME" "$PASSWORD" "$tz" "$HOME/.windows" "$HOME/Windows" || {
|
||||||
VERSION: "11"
|
echo "❌ Failed to write the Windows VM configuration."
|
||||||
RAM_SIZE: "$SELECTED_RAM"
|
exit 1
|
||||||
CPU_CORES: "$SELECTED_CORES"
|
}
|
||||||
DISK_SIZE: "$SELECTED_DISK"
|
write_credentials "$USERNAME" "$PASSWORD"
|
||||||
USERNAME: "$USERNAME"
|
|
||||||
PASSWORD: "$PASSWORD"
|
|
||||||
TZ: "$(timedatectl show -p Timezone --value 2>/dev/null || echo UTC)"
|
|
||||||
ARGUMENTS: "-rtc base=localtime,clock=host,driftfix=slew"
|
|
||||||
devices:
|
|
||||||
- /dev/kvm
|
|
||||||
- /dev/net/tun
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
ports:
|
|
||||||
- 127.0.0.1:8006:8006
|
|
||||||
- 127.0.0.1:3389:3389/tcp
|
|
||||||
- 127.0.0.1:3389:3389/udp
|
|
||||||
volumes:
|
|
||||||
- $HOME/.windows:/storage
|
|
||||||
- $HOME/Windows:/shared
|
|
||||||
restart: "no"
|
|
||||||
stop_grace_period: 2m
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Starting Windows VM installation..."
|
echo "Starting Windows VM installation..."
|
||||||
@@ -210,14 +561,12 @@ EOF
|
|||||||
echo "Monitor installation progress at: http://127.0.0.1:8006"
|
echo "Monitor installation progress at: http://127.0.0.1:8006"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Start docker-compose with user's config
|
|
||||||
echo "Starting Windows VM with docker-compose..."
|
echo "Starting Windows VM with docker-compose..."
|
||||||
if ! docker-compose -f "$COMPOSE_FILE" up -d 2>&1; then
|
if ! priv up; then
|
||||||
echo "❌ Failed to start Windows VM!"
|
echo "❌ Failed to start Windows VM!"
|
||||||
echo " Common issues:"
|
echo " Common issues:"
|
||||||
echo " - Docker daemon not running: sudo systemctl start docker"
|
echo " - Docker daemon not running: sudo systemctl start docker"
|
||||||
echo " - Port already in use: check if another VM is running"
|
echo " - Port already in use: check if another VM is running"
|
||||||
echo " - Permission issues: make sure you're in the docker group"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -237,7 +586,6 @@ EOF
|
|||||||
echo "Once finished, launch 'Windows' via Super + Space"
|
echo "Once finished, launch 'Windows' via Super + Space"
|
||||||
echo ""
|
echo ""
|
||||||
echo "To stop the VM: omarchy-windows-vm stop"
|
echo "To stop the VM: omarchy-windows-vm stop"
|
||||||
echo "To change resources: ~/.config/windows/docker-compose.yml"
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,11 +597,12 @@ remove_windows() {
|
|||||||
|
|
||||||
echo "Removing Windows VM..."
|
echo "Removing Windows VM..."
|
||||||
|
|
||||||
docker-compose -f "$COMPOSE_FILE" down 2>/dev/null || true
|
migrate_legacy_compose 2>/dev/null || true
|
||||||
|
if [[ -f $COMPOSE_FILE ]]; then
|
||||||
|
priv remove || true
|
||||||
|
fi
|
||||||
|
|
||||||
docker rmi dockurr/windows 2>/dev/null || echo "Image already removed or not found"
|
rm -f "$HOME/.local/share/applications/windows-vm.desktop"
|
||||||
|
|
||||||
rm "$HOME/.local/share/applications/windows-vm.desktop"
|
|
||||||
rm -rf "$HOME/.config/windows"
|
rm -rf "$HOME/.config/windows"
|
||||||
rm -rf "$HOME/.windows"
|
rm -rf "$HOME/.windows"
|
||||||
|
|
||||||
@@ -267,49 +616,30 @@ launch_windows() {
|
|||||||
KEEP_ALIVE=true
|
KEEP_ALIVE=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if config exists
|
if ! migrate_legacy_compose; then
|
||||||
if [[ ! -f $COMPOSE_FILE ]]; then
|
if [[ ! -f $COMPOSE_FILE ]]; then
|
||||||
echo "Windows VM not configured. Please run: omarchy-windows-vm install"
|
echo "Windows VM not configured. Please run: omarchy-windows-vm install"
|
||||||
exit 1
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract credentials from compose file
|
# RDP credentials come from the private per-user file. Fall back to the compose
|
||||||
WIN_USER=$(grep "USERNAME:" "$COMPOSE_FILE" | sed 's/.*USERNAME: "\(.*\)"/\1/')
|
# only when it is readable (sudoless mode), reversing the writer's escaping.
|
||||||
WIN_PASS=$(grep "PASSWORD:" "$COMPOSE_FILE" | sed 's/.*PASSWORD: "\(.*\)"/\1/')
|
WIN_USER=$(read_credential USERNAME) || WIN_USER=""
|
||||||
|
WIN_PASS=$(read_credential PASSWORD) || WIN_PASS=""
|
||||||
# Use defaults if not found
|
if [[ -z $WIN_USER || -z $WIN_PASS ]] && [[ -r $COMPOSE_FILE ]]; then
|
||||||
|
[[ -z $WIN_USER ]] && WIN_USER=$(unescape "$(read_compose_value USERNAME "$COMPOSE_FILE")")
|
||||||
|
[[ -z $WIN_PASS ]] && WIN_PASS=$(unescape "$(read_compose_value PASSWORD "$COMPOSE_FILE")")
|
||||||
|
fi
|
||||||
[[ -z $WIN_USER ]] && WIN_USER="docker"
|
[[ -z $WIN_USER ]] && WIN_USER="docker"
|
||||||
[[ -z $WIN_PASS ]] && WIN_PASS="admin"
|
[[ -z $WIN_PASS ]] && WIN_PASS="admin"
|
||||||
|
|
||||||
# Check if container is already running
|
echo "Starting Windows VM (this may prompt for authorization)..."
|
||||||
CONTAINER_STATUS=$(docker inspect --format='{{.State.Status}}' omarchy-windows 2>/dev/null)
|
if ! priv up_wait; then
|
||||||
|
echo "❌ Failed to start Windows VM!"
|
||||||
if [[ $CONTAINER_STATUS != "running" ]]; then
|
echo " Try checking: omarchy-windows-vm status"
|
||||||
echo "Starting Windows VM..."
|
omarchy-notification-send -u critical "Windows VM" "Failed to start Windows VM"
|
||||||
|
exit 1
|
||||||
# Send desktop notification
|
|
||||||
omarchy-notification-send -g "Starting Windows VM" "This can take 15-30 seconds" -t 15000
|
|
||||||
|
|
||||||
if ! docker-compose -f "$COMPOSE_FILE" up -d 2>&1; then
|
|
||||||
echo "❌ Failed to start Windows VM!"
|
|
||||||
echo " Try checking: omarchy-windows-vm status"
|
|
||||||
echo " View logs: docker logs omarchy-windows"
|
|
||||||
omarchy-notification-send -u critical "Windows VM" "Failed to start Windows VM"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Waiting for Windows VM to start..."
|
|
||||||
WAIT_COUNT=0
|
|
||||||
until docker logs omarchy-windows 2>&1 | grep -qi "windows started successfully"; do
|
|
||||||
sleep 2
|
|
||||||
WAIT_COUNT=$((WAIT_COUNT + 1))
|
|
||||||
if (( WAIT_COUNT > 60 )); then # 2 minutes timeout
|
|
||||||
echo ""
|
|
||||||
echo "❌ Timeout: Windows VM failed to start within 2 minutes"
|
|
||||||
echo " Check logs: docker logs omarchy-windows"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build the connection info
|
# Build the connection info
|
||||||
@@ -329,14 +659,27 @@ To stop: omarchy-windows-vm stop"
|
|||||||
"" \
|
"" \
|
||||||
"$LIFECYCLE"
|
"$LIFECYCLE"
|
||||||
|
|
||||||
|
# FreeRDP 3 tries Kerberos before NTLM for NLA, and krb5 ships /etc/krb5.conf
|
||||||
|
# as the MIT sample with default_realm = ATHENA.MIT.EDU. Every connect then
|
||||||
|
# goes looking for MIT's KDC: with internet up it fails fast, but off the
|
||||||
|
# network each attempt blocks ~23s and no RDP window is ever drawn. The VM
|
||||||
|
# authenticates against a local Windows account, so point FreeRDP at a
|
||||||
|
# realm-less config and let it fall straight through to NTLM.
|
||||||
|
KRB5_CONF="$HOME/.config/windows/krb5.conf"
|
||||||
|
mkdir -p "$(dirname "$KRB5_CONF")"
|
||||||
|
if [[ ! -f $KRB5_CONF ]]; then
|
||||||
|
printf '[libdefaults]\n dns_lookup_kdc = false\n dns_lookup_realm = false\n' >"$KRB5_CONF"
|
||||||
|
fi
|
||||||
|
export KRB5_CONFIG="$KRB5_CONF"
|
||||||
|
|
||||||
# Detect display scale from Hyprland
|
# Detect display scale from Hyprland
|
||||||
HYPR_SCALE=$(hyprctl monitors -j | jq -r '.[] | select (.focused == true) | .scale')
|
HYPR_SCALE=$(hyprctl monitors -j | jq -r '.[] | select (.focused == true) | .scale')
|
||||||
SCALE_PERCENT=$(echo "$HYPR_SCALE" | awk '{print int($1 * 100)}')
|
SCALE_PERCENT=$(echo "$HYPR_SCALE" | awk '{print int($1 * 100)}')
|
||||||
|
|
||||||
RDP_SCALE=""
|
RDP_SCALE=""
|
||||||
if (( SCALE_PERCENT >= 170 )); then
|
if ((SCALE_PERCENT >= 170)); then
|
||||||
RDP_SCALE="/scale:180"
|
RDP_SCALE="/scale:180"
|
||||||
elif (( SCALE_PERCENT >= 130 )); then
|
elif ((SCALE_PERCENT >= 130)); then
|
||||||
RDP_SCALE="/scale:140"
|
RDP_SCALE="/scale:140"
|
||||||
fi
|
fi
|
||||||
# If scale is less than 130%, don't set any scale (use default 100)
|
# If scale is less than 130%, don't set any scale (use default 100)
|
||||||
@@ -348,8 +691,12 @@ To stop: omarchy-windows-vm stop"
|
|||||||
if [[ $KEEP_ALIVE = "false" ]]; then
|
if [[ $KEEP_ALIVE = "false" ]]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "RDP session closed. Stopping Windows VM..."
|
echo "RDP session closed. Stopping Windows VM..."
|
||||||
docker-compose -f "$COMPOSE_FILE" down
|
if priv down; then
|
||||||
echo "Windows VM stopped."
|
echo "Windows VM stopped."
|
||||||
|
else
|
||||||
|
echo "⚠️ Could not stop the Windows VM (authorization declined?)."
|
||||||
|
echo " It may still be running. Stop it with: omarchy-windows-vm stop"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
echo "RDP session closed. Windows VM is still running."
|
echo "RDP session closed. Windows VM is still running."
|
||||||
@@ -358,24 +705,34 @@ To stop: omarchy-windows-vm stop"
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop_windows() {
|
stop_windows() {
|
||||||
|
migrate_legacy_compose 2>/dev/null || true
|
||||||
if [[ ! -f $COMPOSE_FILE ]]; then
|
if [[ ! -f $COMPOSE_FILE ]]; then
|
||||||
echo "Windows VM not configured."
|
echo "Windows VM not configured."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Stopping Windows VM..."
|
echo "Stopping Windows VM..."
|
||||||
docker-compose -f "$COMPOSE_FILE" down
|
if priv down; then
|
||||||
echo "Windows VM stopped."
|
echo "Windows VM stopped."
|
||||||
|
else
|
||||||
|
echo "⚠️ Could not stop the Windows VM (authorization declined?). It may still be running."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
status_windows() {
|
status_windows() {
|
||||||
|
migrate_legacy_compose 2>/dev/null || true
|
||||||
if [[ ! -f $COMPOSE_FILE ]]; then
|
if [[ ! -f $COMPOSE_FILE ]]; then
|
||||||
echo "Windows VM not configured."
|
echo "Windows VM not configured."
|
||||||
echo "To set up: omarchy-windows-vm install"
|
echo "To set up: omarchy-windows-vm install"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CONTAINER_STATUS=$(docker inspect --format='{{.State.Status}}' omarchy-windows 2>/dev/null)
|
if ! CONTAINER_STATUS=$(priv status); then
|
||||||
|
echo "Could not query the Windows VM (authorization declined?)."
|
||||||
|
echo "To try again: omarchy-windows-vm status"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z $CONTAINER_STATUS ]]; then
|
if [[ -z $CONTAINER_STATUS ]]; then
|
||||||
echo "Windows VM container not found."
|
echo "Windows VM container not found."
|
||||||
@@ -421,28 +778,41 @@ show_usage() {
|
|||||||
|
|
||||||
# Main command dispatcher
|
# Main command dispatcher
|
||||||
case "$1" in
|
case "$1" in
|
||||||
install)
|
__priv)
|
||||||
install_windows
|
((EUID == 0)) || {
|
||||||
;;
|
echo "omarchy-windows-vm __priv must run as root" >&2
|
||||||
remove)
|
|
||||||
remove_windows
|
|
||||||
;;
|
|
||||||
launch|start)
|
|
||||||
launch_windows "$2"
|
|
||||||
;;
|
|
||||||
stop|down)
|
|
||||||
stop_windows
|
|
||||||
;;
|
|
||||||
status)
|
|
||||||
status_windows
|
|
||||||
;;
|
|
||||||
help|--help|-h|"")
|
|
||||||
show_usage
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown command: $1" >&2
|
|
||||||
echo "" >&2
|
|
||||||
show_usage >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
}
|
||||||
|
action="$2"
|
||||||
|
shift 2
|
||||||
|
valid_priv_action "$action" || {
|
||||||
|
echo "omarchy-windows-vm: unknown privileged action" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
"__priv_${action}" "$@"
|
||||||
|
;;
|
||||||
|
install)
|
||||||
|
install_windows
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
remove_windows
|
||||||
|
;;
|
||||||
|
launch | start)
|
||||||
|
launch_windows "$2"
|
||||||
|
;;
|
||||||
|
stop | down)
|
||||||
|
stop_windows
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status_windows
|
||||||
|
;;
|
||||||
|
help | --help | -h | "")
|
||||||
|
show_usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown command: $1" >&2
|
||||||
|
echo "" >&2
|
||||||
|
show_usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -5,8 +5,8 @@ profile {
|
|||||||
identity = true
|
identity = true
|
||||||
}
|
}
|
||||||
|
|
||||||
# To enable auto switch to nightlight, set in your .config/hypr/autostart:
|
# To enable auto switch to nightlight, add to your .config/hypr/autostart.lua:
|
||||||
# exec-once = uwsm app -- hyprsunset
|
# o.launch_on_start("hyprsunset")
|
||||||
# and use the following:
|
# and use the following:
|
||||||
# profile {
|
# profile {
|
||||||
# time = 20:00
|
# time = 20:00
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
# To put it into use, remove .sample from this file name.
|
# To put it into use, remove .sample from this file name.
|
||||||
|
|
||||||
# Example: Show the name of the font that was just set.
|
# Example: Show the name of the font that was just set.
|
||||||
# notify-send -u low "New font" "Your new font is $1"
|
# omarchy-notification-send -u low "New font" "Your new font is $1"
|
||||||
@@ -6,5 +6,5 @@
|
|||||||
weather=$(omarchy-weather-status 2>/dev/null) || true
|
weather=$(omarchy-weather-status 2>/dev/null) || true
|
||||||
|
|
||||||
if [[ -n $weather && $weather != "Weather unavailable" ]]; then
|
if [[ -n $weather && $weather != "Weather unavailable" ]]; then
|
||||||
notify-send -u low "$weather"
|
omarchy-notification-send -u low "$weather"
|
||||||
fi
|
fi
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
# To put it into use, remove .sample from this file name.
|
# To put it into use, remove .sample from this file name.
|
||||||
|
|
||||||
# Example: Show notification after the system has been updated.
|
# Example: Show notification after the system has been updated.
|
||||||
# notify-send -u low "Update Performed" "Your system is now up to date"
|
# omarchy-notification-send -u low "Update Performed" "Your system is now up to date"
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
# To put it into use, remove .sample from this file name.
|
# To put it into use, remove .sample from this file name.
|
||||||
|
|
||||||
# Example: Show the name of the theme that was just set.
|
# Example: Show the name of the theme that was just set.
|
||||||
# notify-send -u low "New theme" "Your new theme is $1"
|
# omarchy-notification-send -u low "New theme" "Your new theme is $1"
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
name: diagnose-crash
|
||||||
|
description: >
|
||||||
|
Diagnose why a program crashed on this machine, from a systemd-coredump core dump.
|
||||||
|
Use when a process has segfaulted, aborted, or otherwise dumped core, when asked
|
||||||
|
why an application crashed or disappeared, or when a "Process crashed:" desktop
|
||||||
|
notification is acted on. Triggers: crash, segfault, SIGSEGV, SIGABRT, core dump,
|
||||||
|
coredumpctl, "why did X crash", "X keeps crashing", backtrace symbolization.
|
||||||
|
Covers reporting a confirmed Omarchy bug upstream — see reporting.md.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Diagnosing a Crash
|
||||||
|
|
||||||
|
Work from evidence. The goal is an honest account of what happened, not a
|
||||||
|
plausible-sounding story.
|
||||||
|
|
||||||
|
## Establish the facts
|
||||||
|
|
||||||
|
`coredumpctl info <pid>` is the starting point. Beyond the backtrace, note the
|
||||||
|
**command line** the process was started with — it usually reveals what the
|
||||||
|
program was working on when it died, which is often the whole answer.
|
||||||
|
|
||||||
|
`coredumpctl list` shows whether this crash is a one-off or a pattern. Repeated
|
||||||
|
crashes of the same program, or several programs dying together, point somewhere
|
||||||
|
different than a single failure does.
|
||||||
|
|
||||||
|
## Rule out the boring causes first
|
||||||
|
|
||||||
|
Check resource exhaustion before blaming the program: `free -h`, and the journal
|
||||||
|
for OOM kills. A process killed by the OOM killer is not a bug in that process.
|
||||||
|
|
||||||
|
## Correlate against the timeline
|
||||||
|
|
||||||
|
The crash timestamp is the most underused piece of evidence. Compare it against:
|
||||||
|
|
||||||
|
- **Filesystem mtimes.** A directory or file whose mtime lands on the same second
|
||||||
|
as the crash strongly suggests what triggered it.
|
||||||
|
- **The journal** around that moment, for related warnings from the same or
|
||||||
|
neighbouring processes.
|
||||||
|
- **Recent package updates.** A crash that starts right after an update points at
|
||||||
|
the update.
|
||||||
|
|
||||||
|
## Read the whole core, not just frame 0
|
||||||
|
|
||||||
|
Thread stacks other than the crashing one show what work was **in flight** —
|
||||||
|
thumbnailers, image loaders, IPC readers, GPU queues. That context often explains
|
||||||
|
the trigger even when the crashing frame itself cannot be symbolized.
|
||||||
|
|
||||||
|
Note any third-party code in the address space: file-manager or browser
|
||||||
|
extensions, plugins, out-of-tree drivers. In-process third-party code is a common
|
||||||
|
crash source and worth flagging — but do not pin blame on it without evidence
|
||||||
|
that it is actually implicated.
|
||||||
|
|
||||||
|
## Symbolize when you can
|
||||||
|
|
||||||
|
This is Arch, which runs a public debuginfod server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
core=$(mktemp -t crash-XXXXXX.core)
|
||||||
|
trap 'rm -f "$core"' EXIT
|
||||||
|
coredumpctl dump <pid> --output="$core"
|
||||||
|
DEBUGINFOD_URLS="https://debuginfod.archlinux.org" \
|
||||||
|
gdb -q <executable> "$core" \
|
||||||
|
-batch -ex 'set debuginfod enabled on' -ex 'bt'
|
||||||
|
```
|
||||||
|
|
||||||
|
A core is a verbatim copy of the process's memory and can hold passwords, tokens,
|
||||||
|
and private documents. Write it to a fresh `mktemp` path rather than a predictable
|
||||||
|
shared one, and delete it when you are done — never leave it lying in `/tmp`.
|
||||||
|
|
||||||
|
Many packages publish no debug symbols. When frames stay unresolved, say so —
|
||||||
|
never invent function names to fill the gap. An unsymbolized stack still has
|
||||||
|
shape: which library each frame belongs to, and whether the crash came from a
|
||||||
|
signal handler, a main loop, or a worker thread.
|
||||||
|
|
||||||
|
## Report
|
||||||
|
|
||||||
|
1. What crashed, and what it was doing at the time.
|
||||||
|
2. The most likely mechanism — separating clearly what the evidence **proves**
|
||||||
|
from what you are **inferring**.
|
||||||
|
3. Whether any user data was lost, and where it can be recovered from. Check the
|
||||||
|
trash before concluding anything is gone.
|
||||||
|
4. Whether it is likely to recur, and what would avoid or fix it.
|
||||||
|
|
||||||
|
Be straight about the limits of the evidence. If the cause is genuinely
|
||||||
|
ambiguous, say so rather than assembling confidence out of guesswork.
|
||||||
|
|
||||||
|
**Leave the system as you found it.** Diagnosis reads; it does not fix, tidy, or
|
||||||
|
reconfigure. The one thing to clean up is your own: delete the core you extracted
|
||||||
|
above, which is a copy of the crashed process's memory.
|
||||||
|
|
||||||
|
## If it is an Omarchy bug
|
||||||
|
|
||||||
|
Most application crashes are upstream bugs in those applications, not Omarchy's
|
||||||
|
doing. In the minority of cases where the cause really does sit within Omarchy's
|
||||||
|
sphere of control, read [`reporting.md`](reporting.md) before offering to file
|
||||||
|
anything.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Reporting a Crash Upstream to Omarchy
|
||||||
|
|
||||||
|
Read this only after concluding that a crash is genuinely Omarchy's to fix.
|
||||||
|
|
||||||
|
## Is it even Omarchy's bug?
|
||||||
|
|
||||||
|
Be strict here. Omarchy is a configuration layer over Arch Linux, so a crash
|
||||||
|
inside a third-party application — a file manager, a browser, a GNOME or Qt
|
||||||
|
library — is almost always an upstream bug in **that** project, not in Omarchy.
|
||||||
|
|
||||||
|
Omarchy's sphere of control is roughly:
|
||||||
|
|
||||||
|
- the `omarchy-*` commands
|
||||||
|
- the Quickshell shell and its plugins
|
||||||
|
- the Hyprland and terminal configuration it ships
|
||||||
|
- its themes
|
||||||
|
- its install and migration scripts
|
||||||
|
- how it packages and configures what it installs
|
||||||
|
|
||||||
|
A crash in a program Omarchy merely installs is **not** an Omarchy bug unless
|
||||||
|
Omarchy's own packaging or configuration is implicated.
|
||||||
|
|
||||||
|
If it is not Omarchy's, say so and stop. Suggesting the right upstream project is
|
||||||
|
useful; filing there yourself is not part of this.
|
||||||
|
|
||||||
|
## Three conditions, all required
|
||||||
|
|
||||||
|
1. **It is a verified bug in Omarchy's sphere**, established on evidence. Issues
|
||||||
|
are for verified bugs only. An "is this even a bug?" belongs on the Discord at
|
||||||
|
<https://omarchy.org/discord>; a feature idea belongs in GitHub Discussions
|
||||||
|
under Suggestions.
|
||||||
|
2. **The user has explicitly agreed.** Show them the exact title and body you
|
||||||
|
propose, and wait for a yes. Never file unprompted.
|
||||||
|
3. **The machine can file it** — `gh auth status` must succeed. If `gh` is missing
|
||||||
|
or unauthenticated, do not install or authenticate it. Say so, and hand the
|
||||||
|
user the finished text to submit themselves.
|
||||||
|
|
||||||
|
## Search before filing
|
||||||
|
|
||||||
|
A duplicate issue costs a maintainer more time than no report at all.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh search issues --repo basecamp/omarchy "<program> crash"
|
||||||
|
gh issue list --repo basecamp/omarchy --state all --search "<signal> <program>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Search on the crashing program, the signal, and distinctive symbols from the
|
||||||
|
backtrace — not on the wording of the title you were about to write.
|
||||||
|
|
||||||
|
`gh search issues` accepts only `open` or `closed` for `--state`, and errors on
|
||||||
|
anything else. Leaving it off searches both, which is what you want here.
|
||||||
|
|
||||||
|
Include **closed** issues. A matching issue closed as fixed, when the crash still
|
||||||
|
reproduces on a current system, is a regression — and reporting that is worth far
|
||||||
|
more than another duplicate.
|
||||||
|
|
||||||
|
## Adding to an existing report
|
||||||
|
|
||||||
|
If a plausible match comes back, read it properly first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh issue view <number> --repo basecamp/omarchy --comments
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm it is genuinely the same failure. The same program crashing is not the
|
||||||
|
same bug if the trigger or the stack differs.
|
||||||
|
|
||||||
|
If it is the same, add to that issue rather than opening a new one — but only
|
||||||
|
when you have something the thread does not already contain: a different
|
||||||
|
reproduction, a symbolized stack where it has none, a narrower trigger, a version
|
||||||
|
where it regressed.
|
||||||
|
|
||||||
|
A comment that only says the bug happens to you too is noise. If that is all you
|
||||||
|
have, tell the user so and file nothing.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh issue comment <number> --repo basecamp/omarchy --body "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Filing a new issue
|
||||||
|
|
||||||
|
Only when the search turns up nothing that matches:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh issue create --repo basecamp/omarchy --title "..." --body "..."
|
||||||
|
```
|
||||||
|
|
||||||
|
Include what happened, what was expected, steps to reproduce, system details from
|
||||||
|
`omarchy version`, and diagnostics from `omarchy debug --no-sudo --print` (which
|
||||||
|
also writes `/tmp/omarchy-debug.log`; the interactive `omarchy debug` can upload
|
||||||
|
it and print a shareable URL worth including).
|
||||||
|
|
||||||
|
`gh` cannot attach media. If a screenshot would help, save one and give the user
|
||||||
|
the path to drag into the web form.
|
||||||
|
|
||||||
|
## Signing
|
||||||
|
|
||||||
|
End the issue or comment with a line naming the model and agent harness that
|
||||||
|
produced it, so a human reader knows it was machine-authored:
|
||||||
|
|
||||||
|
> Filed by \<model name\> via \<agent harness\>.
|
||||||
|
|
||||||
|
Use your actual model and harness names. If you are not certain of them, say so
|
||||||
|
plainly rather than inventing a version string.
|
||||||
@@ -22,11 +22,34 @@ omarchy theme install <url> # Install from git repo
|
|||||||
Additional user backgrounds for any theme (stock or custom) go in
|
Additional user backgrounds for any theme (stock or custom) go in
|
||||||
`~/.config/omarchy/backgrounds/<theme-slug>/`.
|
`~/.config/omarchy/backgrounds/<theme-slug>/`.
|
||||||
|
|
||||||
|
## What a Theme Installed From a Repo May Not Contain
|
||||||
|
|
||||||
|
A theme the user wrote by hand in `~/.config/omarchy/themes` is unrestricted, as
|
||||||
|
are Omarchy's own themes. From a theme cloned by `omarchy theme install`, Omarchy
|
||||||
|
drops only what runs code: any `*.lua` (Hyprland requires a theme's
|
||||||
|
`hyprland.lua` and `gum_env.lua` at login, Neovim loads `neovim.lua` at startup),
|
||||||
|
the terminal configs `alacritty.toml`, `foot.ini`, `ghostty.conf` and
|
||||||
|
`kitty.conf` (each names the program the terminal launches), and `vscode.json`
|
||||||
|
(names a VS Code extension to install). Those are regenerated from `colors.toml`
|
||||||
|
through `$OMARCHY_PATH/default/themed/*.tpl`, and named on stderr.
|
||||||
|
|
||||||
|
Everything else a cloned theme ships is kept, including `btop.theme`,
|
||||||
|
`chromium.theme`, `helix.toml`, `icons.theme`, `keyboard.rgb` and `shell.toml`.
|
||||||
|
Omarchy tells a cloned theme from the user's own by the `.git` directory a clone
|
||||||
|
leaves behind.
|
||||||
|
|
||||||
|
To change how Omarchy themes an app for every theme, write the template rather
|
||||||
|
than the theme: `~/.config/omarchy/themed/<config-name>.tpl` overrides the
|
||||||
|
built-in one. See `docs/theming.md` in the Omarchy repo.
|
||||||
|
|
||||||
## Customizing a Stock Theme
|
## Customizing a Stock Theme
|
||||||
|
|
||||||
Never edit stock themes under `/usr/share/omarchy/themes/` — changes are lost
|
Never edit stock themes under `/usr/share/omarchy/themes/` — changes are lost
|
||||||
on update. Two safe options:
|
on update. Two safe options:
|
||||||
|
|
||||||
|
Both write into `~/.config/omarchy/themes`, where a theme the user wrote is
|
||||||
|
unrestricted — the list above applies only to a theme cloned from a repo.
|
||||||
|
|
||||||
**Overlay (preferred for small tweaks):** create a user theme directory with
|
**Overlay (preferred for small tweaks):** create a user theme directory with
|
||||||
the SAME slug containing only the files you want to change. When the theme is
|
the SAME slug containing only the files you want to change. When the theme is
|
||||||
applied, the stock theme is copied first and your files win on top:
|
applied, the stock theme is copied first and your files win on top:
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ alias ...='cd ../..'
|
|||||||
alias ....='cd ../../..'
|
alias ....='cd ../../..'
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
alias a='omarchy-launch-agent --inline'
|
alias a='omarchy-agent --inline'
|
||||||
alias c='opencode --auto'
|
alias c='opencode --auto'
|
||||||
alias cx='printf "\033[2J\033[3J\033[H" && claude --permission-mode bypassPermissions'
|
alias cx='printf "\033[2J\033[3J\033[H" && claude --permission-mode auto'
|
||||||
alias cy='codex -s danger-full-access -a never'
|
alias cy='codex --approve-for-me'
|
||||||
alias d='docker'
|
alias d='docker'
|
||||||
alias r='rails'
|
alias r='rails'
|
||||||
alias t='tmux attach || tmux new -s Work'
|
alias t='tmux attach || tmux new -s Work'
|
||||||
|
|||||||
@@ -2,12 +2,19 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "WhatsApp Slim",
|
"name": "WhatsApp Slim",
|
||||||
"version": "1.1",
|
"version": "1.1",
|
||||||
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAszLhFYxjvKqx1VAORt3M6b0H1Kz03cmQ1MrUjJZ8gj3nhRcvufk6PS17yUIjEdKKmuj+1HuFVwOaHJQsACGJXwn2TeKQqZmZx/83yd7ydgabHXTsJbhy5O+Yunv+VA1B2IX/phcXpkH9bPv2PuC/bQ13nmmAvT2b+EMyq8EPQHOoSWhE4M4kIhLDczTmxfJX5X3tTxH66952DYuQWvGpIXLAUXkvIgap0qemW68/LymPaWzcg6g3oE/nZ7C0j7JE5bnqLFxqXNlA9Jt+Ss/1Y33QL3OSD9EEsrpgueS0bbqtkmFQ9/jGlruIL+qj0xe+CAtqbJ5ty9H/sqi01kjfyQIDAQAB",
|
||||||
"description": "Make WhatsApp Web follow the system theme and collapse its chat list in narrow windows, this extension is installed by Omarchy",
|
"description": "Make WhatsApp Web follow the system theme and collapse its chat list in narrow windows, this extension is installed by Omarchy",
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://web.whatsapp.com/*"],
|
"matches": [
|
||||||
"css": ["whatsapp.css"],
|
"https://web.whatsapp.com/*"
|
||||||
"js": ["system-theme.js"],
|
],
|
||||||
|
"css": [
|
||||||
|
"whatsapp.css"
|
||||||
|
],
|
||||||
|
"js": [
|
||||||
|
"system-theme.js"
|
||||||
|
],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ The private-use glyphs in `omarchy.ttf` are:
|
|||||||
- `U+E903` — omp, from <https://omp.sh/favicon.svg>
|
- `U+E903` — omp, from <https://omp.sh/favicon.svg>
|
||||||
- `U+E904` — Grok, from <https://grok.com/images/favicon.svg>
|
- `U+E904` — Grok, from <https://grok.com/images/favicon.svg>
|
||||||
- `U+E905` — Codex, from the OpenAI mark on <https://developers.openai.com/codex/>
|
- `U+E905` — Codex, from the OpenAI mark on <https://developers.openai.com/codex/>
|
||||||
|
- `U+E906` — LM Studio, from <https://simpleicons.org/icons/lmstudio.svg>
|
||||||
|
- `U+E907` — Ollama, from <https://simpleicons.org/icons/ollama.svg>
|
||||||
|
|
||||||
The agent marks are monochrome so the menu can render them using the active
|
The agent marks are monochrome so the menu can render them using the active
|
||||||
theme's foreground and selection colors.
|
theme's foreground and selection colors.
|
||||||
Binary file not shown.
@@ -20,8 +20,9 @@ o.window({
|
|||||||
}, { tag = "+floating-window" })
|
}, { tag = "+floating-window" })
|
||||||
|
|
||||||
-- The About fastfetch layout needs more columns than the standard float provides.
|
-- The About fastfetch layout needs more columns than the standard float provides.
|
||||||
-- This size only positions the first paint: omarchy-launch-about then measures
|
-- This size only covers the first launch: omarchy-launch-about measures the
|
||||||
-- the rendered content and fits the window to it exactly.
|
-- rendered content, remembers the size that hugs it, and applies that as its own
|
||||||
|
-- rule before every later launch.
|
||||||
o.window("org.omarchy.about", { float = true })
|
o.window("org.omarchy.about", { float = true })
|
||||||
o.window("org.omarchy.about", { center = true })
|
o.window("org.omarchy.about", { center = true })
|
||||||
o.window("org.omarchy.about", { size = { 920, 480 } })
|
o.window("org.omarchy.about", { size = { 920, 480 } })
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ if o.preinstalled_bindings_enabled() then
|
|||||||
o.bind("SUPER + CTRL + RETURN", "Herdr", { omarchy = "terminal-herdr" })
|
o.bind("SUPER + CTRL + RETURN", "Herdr", { omarchy = "terminal-herdr" })
|
||||||
o.bind("SUPER + SHIFT + M", "Music", { omarchy = "spotify" })
|
o.bind("SUPER + SHIFT + M", "Music", { omarchy = "spotify" })
|
||||||
o.bind("SUPER + SHIFT + ALT + M", "Music TUI", { tui = "cliamp", focus = true })
|
o.bind("SUPER + SHIFT + ALT + M", "Music TUI", { tui = "cliamp", focus = true })
|
||||||
o.bind("SUPER + SHIFT + D", "Docker", { tui = "lazydocker" })
|
o.bind("SUPER + SHIFT + D", "Docker", { tui = "omarchy-launch-docker-tui" })
|
||||||
o.bind("SUPER + SHIFT + G", "Signal", { omarchy = "signal" })
|
o.bind("SUPER + SHIFT + G", "Signal", { omarchy = "signal" })
|
||||||
o.bind("SUPER + SHIFT + O", "Obsidian", { launch = "obsidian", focus = "^obsidian$" })
|
o.bind("SUPER + SHIFT + O", "Obsidian", { launch = "obsidian", focus = "^obsidian$" })
|
||||||
o.bind("SUPER + SHIFT + W", "Omawrite", { launch = "omawrite" })
|
o.bind("SUPER + SHIFT + W", "Omawrite", { launch = "omawrite" })
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ o.bind("SUPER + CTRL + ALT + T", "Show time", "omarchy-notification-time")
|
|||||||
o.bind("SUPER + CTRL + ALT + B", "Show battery remaining", "omarchy-notification-battery")
|
o.bind("SUPER + CTRL + ALT + B", "Show battery remaining", "omarchy-notification-battery")
|
||||||
o.bind("SUPER + CTRL + ALT + W", "Toggle weather", "omarchy-notification-weather")
|
o.bind("SUPER + CTRL + ALT + W", "Toggle weather", "omarchy-notification-weather")
|
||||||
|
|
||||||
o.bind("SUPER + SHIFT + CTRL + A", "Agent", "omarchy-launch-agent")
|
o.bind("SUPER + SHIFT + CTRL + A", "Agent", "omarchy-agent --pick")
|
||||||
o.bind("SUPER + CTRL + A", "Audio", "omarchy-shell shell toggle omarchy.audio")
|
o.bind("SUPER + CTRL + A", "Audio", "omarchy-shell shell toggle omarchy.audio")
|
||||||
o.bind("SUPER + CTRL + B", "Bluetooth", "omarchy-shell shell toggle omarchy.bluetooth")
|
o.bind("SUPER + CTRL + B", "Bluetooth", "omarchy-shell shell toggle omarchy.bluetooth")
|
||||||
o.bind("SUPER + CTRL + D", "Display", "omarchy-shell shell toggle omarchy.monitor")
|
o.bind("SUPER + CTRL + D", "Display", "omarchy-shell shell toggle omarchy.monitor")
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
-- Disable a Hyprland input device whose name was stored as data, not Lua.
|
||||||
|
-- Device names come from USB descriptors and must never be loaded as code.
|
||||||
|
|
||||||
|
local paths = require("default.hypr.paths")
|
||||||
|
|
||||||
|
return function(kind)
|
||||||
|
-- Hardcoded to ~/.local/state to match omarchy-toggle-input-device and the
|
||||||
|
-- sibling bash toggle tools, which all write there regardless of
|
||||||
|
-- XDG_STATE_HOME.
|
||||||
|
local file = io.open(paths.home .. "/.local/state/omarchy/toggles/hypr/" .. kind .. "-disabled-name", "r")
|
||||||
|
if not file then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local name = file:read("*l")
|
||||||
|
file:close()
|
||||||
|
|
||||||
|
if name and name ~= "" then
|
||||||
|
hl.device({ name = name, enabled = false })
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -18,9 +18,20 @@ local function file_exists(path)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Hyprland reaps its own children, so os.execute() can't retrieve an exit status
|
||||||
|
-- from inside the compositor. Read a marker off stdout instead.
|
||||||
function o.shell_succeeds(command)
|
function o.shell_succeeds(command)
|
||||||
local ok, _, code = os.execute(command .. " >/dev/null 2>&1")
|
-- Subshell, so the redirection covers every command rather than binding to
|
||||||
return ok == true or ok == 0 or code == 0
|
-- the last one and letting an earlier one write its own OK into the pipe.
|
||||||
|
local pipe = io.popen("( " .. command .. " ) >/dev/null 2>&1 && echo OK")
|
||||||
|
if not pipe then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local output = pipe:read("*a") or ""
|
||||||
|
pipe:close()
|
||||||
|
|
||||||
|
return output:find("OK", 1, true) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function o.cmd_present(command)
|
function o.cmd_present(command)
|
||||||
@@ -125,7 +136,7 @@ function o.bind_toggle(keys, description, toggle, options)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function o.notify(message)
|
function o.notify(message)
|
||||||
return "notify-send -u low " .. shell_quote(message)
|
return "omarchy-notification-send -u low " .. shell_quote(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
function o.window(match, rules)
|
function o.window(match, rules)
|
||||||
|
|||||||
+13
-3
@@ -4,9 +4,19 @@
|
|||||||
|
|
||||||
local home = os.getenv("HOME")
|
local home = os.getenv("HOME")
|
||||||
|
|
||||||
|
-- A variable that is set but empty means "unset" (XDG Base Directory spec);
|
||||||
|
-- bash's ${VAR:-fallback} in the sibling tools treats it the same way.
|
||||||
|
local function env_or(name, fallback)
|
||||||
|
local value = os.getenv(name)
|
||||||
|
if value == nil or value == "" then
|
||||||
|
return fallback
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
return {
|
return {
|
||||||
home = home,
|
home = home,
|
||||||
config_home = os.getenv("XDG_CONFIG_HOME") or (home .. "/.config"),
|
config_home = env_or("XDG_CONFIG_HOME", home .. "/.config"),
|
||||||
state_home = os.getenv("XDG_STATE_HOME") or (home .. "/.local/state"),
|
state_home = env_or("XDG_STATE_HOME", home .. "/.local/state"),
|
||||||
omarchy_path = os.getenv("OMARCHY_PATH") or "/usr/share/omarchy",
|
omarchy_path = env_or("OMARCHY_PATH", "/usr/share/omarchy"),
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
-- Pass a module prefix for normal package.path modules, e.g.
|
-- Pass a module prefix for normal package.path modules, e.g.
|
||||||
-- require_all.files(paths.omarchy_path .. "/default/hypr/apps", "default.hypr.apps")
|
-- require_all.files(paths.omarchy_path .. "/default/hypr/apps", "default.hypr.apps")
|
||||||
-- Pass nil as the prefix when the directory itself has been added to package.path.
|
-- Pass nil as the prefix when the directory itself has been added to package.path.
|
||||||
|
-- Pass options.exclude as a set of base names (without ".lua") to skip; a legacy
|
||||||
|
-- file that must never be loaded as code stays on disk for a migration to remove.
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -12,19 +14,23 @@ local function shell_quote(path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function M.files(dir, module_prefix, options)
|
function M.files(dir, module_prefix, options)
|
||||||
|
local exclude = options and options.exclude or {}
|
||||||
local handle = io.popen("find " .. shell_quote(dir) .. " -maxdepth 1 -type f -name '*.lua' -printf '%f\\n' 2>/dev/null | sort")
|
local handle = io.popen("find " .. shell_quote(dir) .. " -maxdepth 1 -type f -name '*.lua' -printf '%f\\n' 2>/dev/null | sort")
|
||||||
if handle then
|
if handle then
|
||||||
for filename in handle:lines() do
|
for filename in handle:lines() do
|
||||||
local module = filename:gsub("%.lua$", "")
|
local name = filename:gsub("%.lua$", "")
|
||||||
if module_prefix then
|
if not exclude[name] then
|
||||||
module = module_prefix .. "." .. module
|
local module = name
|
||||||
end
|
if module_prefix then
|
||||||
|
module = module_prefix .. "." .. module
|
||||||
|
end
|
||||||
|
|
||||||
if options and options.reload then
|
if options and options.reload then
|
||||||
package.loaded[module] = nil
|
package.loaded[module] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
require(module)
|
require(module)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
handle:close()
|
handle:close()
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,20 @@ local require_all = require("default.hypr.require_all")
|
|||||||
local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
|
local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
|
||||||
package.path = toggles_dir .. "/?.lua;" .. package.path
|
package.path = toggles_dir .. "/?.lua;" .. package.path
|
||||||
|
|
||||||
require_all.files(toggles_dir, nil, { reload = true })
|
-- touchpad-disabled.lua / touchscreen-disabled.lua were generated Lua in older
|
||||||
|
-- versions and could carry an injected USB device name. They must never be loaded
|
||||||
|
-- as code again: exclude them so a not-yet-migrated install cannot execute a
|
||||||
|
-- leftover payload on reload. The migration recovers the name and deletes them.
|
||||||
|
require_all.files(toggles_dir, nil, {
|
||||||
|
reload = true,
|
||||||
|
exclude = {
|
||||||
|
["touchpad-disabled"] = true,
|
||||||
|
["touchscreen-disabled"] = true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
local disabled_input_device = require("default.hypr.disabled-input-device")
|
||||||
|
disabled_input_device("touchpad")
|
||||||
|
disabled_input_device("touchscreen")
|
||||||
|
|
||||||
require("default.hypr.workspace-layouts")
|
require("default.hypr.workspace-layouts")
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
// Learn
|
// Learn
|
||||||
"learn.keybindings": {"icon":"","label":"Keybindings","action":"omarchy-menu-keybindings"},
|
"learn.keybindings": {"icon":"","label":"Keybindings","action":"omarchy-menu-keybindings"},
|
||||||
"learn.omarchy": {"icon":"","label":"Omarchy","action":"omarchy-launch-webapp 'https://learn.omacom.io/2/the-omarchy-manual'"},
|
"learn.omarchy": {"icon":"","label":"Omarchy","action":"omarchy-launch-webapp 'https://omarchy.org/manual/'"},
|
||||||
"learn.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-webapp 'https://wiki.hypr.land/'"},
|
"learn.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-webapp 'https://wiki.hypr.land/'"},
|
||||||
"learn.arch": {"icon":"","label":"Arch","action":"omarchy-launch-webapp 'https://wiki.archlinux.org/title/Main_page'"},
|
"learn.arch": {"icon":"","label":"Arch","action":"omarchy-launch-webapp 'https://wiki.archlinux.org/title/Main_page'"},
|
||||||
"learn.neovim": {"icon":"","label":"Neovim","action":"omarchy-launch-webapp 'https://www.lazyvim.org/keymaps'"},
|
"learn.neovim": {"icon":"","label":"Neovim","action":"omarchy-launch-webapp 'https://www.lazyvim.org/keymaps'"},
|
||||||
@@ -84,6 +84,7 @@
|
|||||||
"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.idle-lock": {"icon":"","label":"Stay Awake","action":"omarchy-toggle-idle"},
|
||||||
"trigger.toggle.notifications": {"icon":"","label":"Notifications","action":"omarchy-toggle-notification-silencing"},
|
"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.screensaver": {"icon":"","label":"Screensaver","action":"omarchy-toggle-screensaver"},
|
||||||
"trigger.toggle.nightlight": {"icon":"","label":"Nightlight","action":"omarchy-toggle-nightlight"},
|
"trigger.toggle.nightlight": {"icon":"","label":"Nightlight","action":"omarchy-toggle-nightlight"},
|
||||||
"trigger.toggle.top-bar": {"icon":"","label":"Menu Bar","action":"omarchy-toggle-bar"},
|
"trigger.toggle.top-bar": {"icon":"","label":"Menu Bar","action":"omarchy-toggle-bar"},
|
||||||
@@ -128,7 +129,7 @@
|
|||||||
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
|
"setup.network.dns.custom": {"icon":"","label":"Custom","checked":"[[ \"$(omarchy-dns)\" == \"Custom\" ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-dns Custom'"},
|
||||||
"setup.network.qr": {"icon":"","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell shell summon omarchy.wifiqr"},
|
"setup.network.qr": {"icon":"","label":"QR Code","aliases":["wifi-qr"],"when":"[[ $(omarchy-network-status) == wifi* ]]","action":"omarchy-shell shell summon omarchy.wifiqr"},
|
||||||
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
|
||||||
"setup.default.agent": {"icon":"","label":"Agent"},
|
"setup.default.agent": {"icon":"","label":"Agent","title":"Default Agent"},
|
||||||
"setup.default.agent.claude": {"icon":"","label":"Claude","checked":"[[ \"$(omarchy-default-agent)\" == \"claude\" ]]","action":"omarchy-default-agent claude"},
|
"setup.default.agent.claude": {"icon":"","label":"Claude","checked":"[[ \"$(omarchy-default-agent)\" == \"claude\" ]]","action":"omarchy-default-agent claude"},
|
||||||
"setup.default.agent.codex": {"icon":"","iconFont":"omarchy","label":"Codex","checked":"[[ \"$(omarchy-default-agent)\" == \"codex\" ]]","action":"omarchy-default-agent codex"},
|
"setup.default.agent.codex": {"icon":"","iconFont":"omarchy","label":"Codex","checked":"[[ \"$(omarchy-default-agent)\" == \"codex\" ]]","action":"omarchy-default-agent codex"},
|
||||||
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
|
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
|
||||||
@@ -138,7 +139,7 @@
|
|||||||
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
|
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
|
||||||
"setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"},
|
"setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"},
|
||||||
"setup.default.agent.pi": {"icon":"","iconFont":"omarchy","label":"Pi","checked":"[[ \"$(omarchy-default-agent)\" == \"pi\" ]]","action":"omarchy-default-agent pi"},
|
"setup.default.agent.pi": {"icon":"","iconFont":"omarchy","label":"Pi","checked":"[[ \"$(omarchy-default-agent)\" == \"pi\" ]]","action":"omarchy-default-agent pi"},
|
||||||
"setup.default.browser": {"icon":"","label":"Browser"},
|
"setup.default.browser": {"icon":"","label":"Browser","title":"Default Browser"},
|
||||||
"setup.default.browser.chromium": {"icon":"","label":"Chromium","when":"omarchy-cmd-present chromium","checked":"[[ \"$(omarchy-default-browser)\" == \"chromium\" ]]","action":"omarchy-default-browser chromium"},
|
"setup.default.browser.chromium": {"icon":"","label":"Chromium","when":"omarchy-cmd-present chromium","checked":"[[ \"$(omarchy-default-browser)\" == \"chromium\" ]]","action":"omarchy-default-browser chromium"},
|
||||||
"setup.default.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-cmd-present google-chrome-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"chrome\" ]]","action":"omarchy-default-browser chrome"},
|
"setup.default.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-cmd-present google-chrome-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"chrome\" ]]","action":"omarchy-default-browser chrome"},
|
||||||
"setup.default.browser.brave": {"icon":"","label":"Brave","when":"omarchy-cmd-present brave","checked":"[[ \"$(omarchy-default-browser)\" == \"brave\" ]]","action":"omarchy-default-browser brave"},
|
"setup.default.browser.brave": {"icon":"","label":"Brave","when":"omarchy-cmd-present brave","checked":"[[ \"$(omarchy-default-browser)\" == \"brave\" ]]","action":"omarchy-default-browser brave"},
|
||||||
@@ -146,12 +147,12 @@
|
|||||||
"setup.default.browser.edge": {"icon":"","label":"Edge","when":"omarchy-cmd-present microsoft-edge-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"edge\" ]]","action":"omarchy-default-browser edge"},
|
"setup.default.browser.edge": {"icon":"","label":"Edge","when":"omarchy-cmd-present microsoft-edge-stable","checked":"[[ \"$(omarchy-default-browser)\" == \"edge\" ]]","action":"omarchy-default-browser edge"},
|
||||||
"setup.default.browser.firefox": {"icon":"","label":"Firefox","when":"omarchy-cmd-present firefox","checked":"[[ \"$(omarchy-default-browser)\" == \"firefox\" ]]","action":"omarchy-default-browser firefox"},
|
"setup.default.browser.firefox": {"icon":"","label":"Firefox","when":"omarchy-cmd-present firefox","checked":"[[ \"$(omarchy-default-browser)\" == \"firefox\" ]]","action":"omarchy-default-browser firefox"},
|
||||||
"setup.default.browser.zen": {"icon":"","label":"Zen","when":"omarchy-cmd-present zen-browser","checked":"[[ \"$(omarchy-default-browser)\" == \"zen\" ]]","action":"omarchy-default-browser zen"},
|
"setup.default.browser.zen": {"icon":"","label":"Zen","when":"omarchy-cmd-present zen-browser","checked":"[[ \"$(omarchy-default-browser)\" == \"zen\" ]]","action":"omarchy-default-browser zen"},
|
||||||
"setup.default.terminal": {"icon":"","label":"Terminal"},
|
"setup.default.terminal": {"icon":"","label":"Terminal","title":"Default Terminal"},
|
||||||
"setup.default.terminal.alacritty": {"icon":"","label":"Alacritty","when":"omarchy-cmd-present alacritty","checked":"[[ \"$(omarchy-default-terminal)\" == \"alacritty\" ]]","action":"omarchy-default-terminal alacritty"},
|
"setup.default.terminal.alacritty": {"icon":"","label":"Alacritty","when":"omarchy-cmd-present alacritty","checked":"[[ \"$(omarchy-default-terminal)\" == \"alacritty\" ]]","action":"omarchy-default-terminal alacritty"},
|
||||||
"setup.default.terminal.foot": {"icon":"","label":"Foot","when":"omarchy-cmd-present foot","checked":"[[ \"$(omarchy-default-terminal)\" == \"foot\" ]]","action":"omarchy-default-terminal foot"},
|
"setup.default.terminal.foot": {"icon":"","label":"Foot","when":"omarchy-cmd-present foot","checked":"[[ \"$(omarchy-default-terminal)\" == \"foot\" ]]","action":"omarchy-default-terminal foot"},
|
||||||
"setup.default.terminal.ghostty": {"icon":"","label":"Ghostty","when":"omarchy-cmd-present ghostty","checked":"[[ \"$(omarchy-default-terminal)\" == \"ghostty\" ]]","action":"omarchy-default-terminal ghostty"},
|
"setup.default.terminal.ghostty": {"icon":"","label":"Ghostty","when":"omarchy-cmd-present ghostty","checked":"[[ \"$(omarchy-default-terminal)\" == \"ghostty\" ]]","action":"omarchy-default-terminal ghostty"},
|
||||||
"setup.default.terminal.kitty": {"icon":"","label":"Kitty","when":"omarchy-cmd-present kitty","checked":"[[ \"$(omarchy-default-terminal)\" == \"kitty\" ]]","action":"omarchy-default-terminal kitty"},
|
"setup.default.terminal.kitty": {"icon":"","label":"Kitty","when":"omarchy-cmd-present kitty","checked":"[[ \"$(omarchy-default-terminal)\" == \"kitty\" ]]","action":"omarchy-default-terminal kitty"},
|
||||||
"setup.default.editor": {"icon":"","label":"Editor"},
|
"setup.default.editor": {"icon":"","label":"Editor","title":"Default Editor"},
|
||||||
"setup.default.editor.neovim": {"icon":"","label":"Neovim","when":"omarchy-cmd-present nvim","checked":"[[ \"$(omarchy-default-editor)\" == \"nvim\" ]]","action":"omarchy-default-editor nvim"},
|
"setup.default.editor.neovim": {"icon":"","label":"Neovim","when":"omarchy-cmd-present nvim","checked":"[[ \"$(omarchy-default-editor)\" == \"nvim\" ]]","action":"omarchy-default-editor nvim"},
|
||||||
"setup.default.editor.vscode": {"icon":"","label":"VSCode","when":"omarchy-cmd-present code","checked":"[[ \"$(omarchy-default-editor)\" == \"code\" ]]","action":"omarchy-default-editor code"},
|
"setup.default.editor.vscode": {"icon":"","label":"VSCode","when":"omarchy-cmd-present code","checked":"[[ \"$(omarchy-default-editor)\" == \"code\" ]]","action":"omarchy-default-editor code"},
|
||||||
"setup.default.editor.cursor": {"icon":"","label":"Cursor","when":"omarchy-cmd-present cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"},
|
"setup.default.editor.cursor": {"icon":"","label":"Cursor","when":"omarchy-cmd-present cursor","checked":"[[ \"$(omarchy-default-editor)\" == \"cursor\" ]]","action":"omarchy-default-editor cursor"},
|
||||||
@@ -172,6 +173,7 @@
|
|||||||
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
"setup.security.fido2": {"icon":"","label":"Fido2","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
|
||||||
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
"setup.security.sshd": {"icon":"","label":"SSHD","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sshd"},
|
||||||
"setup.security.passwordless-sudo": {"icon":"","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
|
"setup.security.passwordless-sudo": {"icon":"","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
|
||||||
|
"setup.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-sudoless-docker"},
|
||||||
"setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""},
|
"setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""},
|
||||||
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
|
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
|
||||||
"setup.config.xcompose": {"icon":"","label":"XCompose","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"},
|
"setup.config.xcompose": {"icon":"","label":"XCompose","action":"omarchy-launch-config-editor ~/.XCompose && omarchy-restart-xcompose"},
|
||||||
@@ -201,6 +203,7 @@
|
|||||||
"install.ai": {"icon":"","label":"AI"},
|
"install.ai": {"icon":"","label":"AI"},
|
||||||
"install.gaming": {"icon":"","label":"Gaming"},
|
"install.gaming": {"icon":"","label":"Gaming"},
|
||||||
"install.windows": {"icon":"","label":"Windows","when":"[[ ! -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
|
"install.windows": {"icon":"","label":"Windows","when":"[[ ! -f $HOME/.local/share/applications/windows-vm.desktop ]]","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-windows-vm install'"},
|
||||||
|
"install.preinstalls": {"icon":"","label":"Preinstalls","when":"[[ -f $HOME/.local/state/omarchy/preinstalls-removed ]]","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-preinstalls"},
|
||||||
"install.browser.chrome": {"icon":"","label":"Chrome","when":"! omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
|
"install.browser.chrome": {"icon":"","label":"Chrome","when":"! omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser chrome'"},
|
||||||
"install.browser.edge": {"icon":"","label":"Edge","when":"! omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
|
"install.browser.edge": {"icon":"","label":"Edge","when":"! omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser edge'"},
|
||||||
"install.browser.brave": {"icon":"","label":"Brave","when":"! omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave'"},
|
"install.browser.brave": {"icon":"","label":"Brave","when":"! omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-browser brave'"},
|
||||||
@@ -227,9 +230,11 @@
|
|||||||
"install.terminal.foot": {"icon":"","label":"Foot","when":"! omarchy-pkg-present foot","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal foot'"},
|
"install.terminal.foot": {"icon":"","label":"Foot","when":"! omarchy-pkg-present foot","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal foot'"},
|
||||||
"install.terminal.ghostty": {"icon":"","label":"Ghostty","when":"! omarchy-pkg-present ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"},
|
"install.terminal.ghostty": {"icon":"","label":"Ghostty","when":"! omarchy-pkg-present ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"},
|
||||||
"install.terminal.kitty": {"icon":"","label":"Kitty","when":"! omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"},
|
"install.terminal.kitty": {"icon":"","label":"Kitty","when":"! omarchy-pkg-present kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"},
|
||||||
|
"install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","when":"! omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"},
|
||||||
"install.ai.dictation": {"icon":"","label":"Dictation","when":"! omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
|
"install.ai.dictation": {"icon":"","label":"Dictation","when":"! omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
|
||||||
"install.ai.lm-studio": {"icon":"","label":"LM Studio","when":"! omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
|
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","when":"! omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
|
||||||
"install.ai.ollama": {"icon":"","label":"Ollama","when":"! omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
|
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","when":"! omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
|
||||||
|
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","when":"! omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
|
||||||
"install.gaming.steam": {"icon":"","label":"Steam","when":"! omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
|
"install.gaming.steam": {"icon":"","label":"Steam","when":"! omarchy-pkg-present steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
|
||||||
"install.gaming.retroarch": {"icon":"","label":"RetroArch","when":"! omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
|
"install.gaming.retroarch": {"icon":"","label":"RetroArch","when":"! omarchy-pkg-present retroarch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
|
||||||
"install.gaming.minecraft": {"icon":"","label":"Minecraft","when":"! omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"},
|
"install.gaming.minecraft": {"icon":"","label":"Minecraft","when":"! omarchy-pkg-present minecraft-launcher","action":"omarchy-install-and-launch Minecraft minecraft-launcher minecraft-launcher"},
|
||||||
@@ -279,6 +284,7 @@
|
|||||||
"remove.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
|
"remove.security.fingerprint": {"icon":"","label":"Fingerprint","when":"omarchy-pkg-present fprintd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fingerprint"},
|
||||||
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
||||||
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
||||||
|
"remove.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
|
||||||
"remove.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser chrome'"},
|
"remove.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser chrome'"},
|
||||||
"remove.browser.edge": {"icon":"","label":"Edge","when":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser edge'"},
|
"remove.browser.edge": {"icon":"","label":"Edge","when":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser edge'"},
|
||||||
"remove.browser.brave": {"icon":"","label":"Brave","when":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser brave'"},
|
"remove.browser.brave": {"icon":"","label":"Brave","when":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser brave'"},
|
||||||
@@ -322,7 +328,7 @@
|
|||||||
"update.omarchy": {"icon":"","iconFont":"omarchy","label":"Omarchy","action":"omarchy-launch-floating-terminal-with-presentation omarchy-update"},
|
"update.omarchy": {"icon":"","iconFont":"omarchy","label":"Omarchy","action":"omarchy-launch-floating-terminal-with-presentation omarchy-update"},
|
||||||
"update.channel": {"icon":"","label":"Channel"},
|
"update.channel": {"icon":"","label":"Channel"},
|
||||||
"update.config": {"icon":"","label":"Config","title":"Reset to default"},
|
"update.config": {"icon":"","label":"Config","title":"Reset to default"},
|
||||||
"update.themes": {"icon":"","label":"Extra Themes","action":"omarchy-launch-floating-terminal-with-presentation omarchy-theme-update"},
|
"update.themes": {"icon":"","label":"Extra Themes","when":"omarchy-theme-extras","action":"omarchy-launch-floating-terminal-with-presentation omarchy-theme-update"},
|
||||||
"update.process": {"icon":"","label":"Process","title":"Restart"},
|
"update.process": {"icon":"","label":"Process","title":"Restart"},
|
||||||
"update.hardware": {"icon":"","label":"Hardware","title":"Restart"},
|
"update.hardware": {"icon":"","label":"Hardware","title":"Restart"},
|
||||||
"update.firmware": {"icon":"","label":"Firmware","action":"omarchy-launch-floating-terminal-with-presentation omarchy-update-firmware"},
|
"update.firmware": {"icon":"","label":"Firmware","action":"omarchy-launch-floating-terminal-with-presentation omarchy-update-firmware"},
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Announce process crashes and offer an AI diagnosis
|
||||||
|
# Needs the session bus to notify, and uwsm-app to open the diagnosis terminal.
|
||||||
|
# Both are up only after graphical-session.target.
|
||||||
|
After=graphical-session.target
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
ConditionEnvironment=WAYLAND_DISPLAY
|
||||||
|
# Set by omarchy-toggle-crash-capture. Checked here so a disabled watcher stays
|
||||||
|
# disabled across logins without the unit having to be disabled.
|
||||||
|
ConditionPathExists=!%h/.local/state/omarchy/toggles/crash-capture-off
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/omarchy-crash-watch
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=graphical-session.target
|
||||||
+39
-1
@@ -6,13 +6,22 @@ Omarchy themes live under `themes/<name>/` in the source tree (installed at
|
|||||||
`colors.toml`; Omarchy generates the active theme files from
|
`colors.toml`; Omarchy generates the active theme files from
|
||||||
`default/themed/*.tpl` when `omarchy-theme-set <name>` runs.
|
`default/themed/*.tpl` when `omarchy-theme-set <name>` runs.
|
||||||
|
|
||||||
|
Beyond `colors.toml` and hand-written config overrides, a first-party theme can
|
||||||
|
ship `backgrounds/` (users overlay their own via
|
||||||
|
`~/.config/omarchy/backgrounds/<name>/`; the active image is the
|
||||||
|
`~/.local/state/omarchy/current/background` symlink), `preview.png` and
|
||||||
|
`preview-unlock.png` for the theme switcher, `icons.theme`, `keyboard.rgb`,
|
||||||
|
`unlock.png`, and a `light.mode` marker file.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
## Theme activation flow
|
## Theme activation flow
|
||||||
|
|
||||||
`omarchy-theme-set <name>` builds a clean staging directory at
|
`omarchy-theme-set <name>` builds a clean staging directory at
|
||||||
`~/.local/state/omarchy/current/next-theme`:
|
`~/.local/state/omarchy/current/next-theme`:
|
||||||
|
|
||||||
1. Copy the first-party theme from `themes/<name>/`.
|
1. Copy the first-party theme from `themes/<name>/`.
|
||||||
2. Overlay any user theme files from `~/.config/omarchy/themes/<name>/`.
|
2. Overlay `~/.config/omarchy/themes/<name>/`, in full when the user wrote it and filtered when it came from a git repo, naming anything it dropped on stderr.
|
||||||
3. If needed, generate `colors.toml` from `alacritty.toml`.
|
3. If needed, generate `colors.toml` from `alacritty.toml`.
|
||||||
4. Run `omarchy-theme-set-templates` to render templates into the staging
|
4. Run `omarchy-theme-set-templates` to render templates into the staging
|
||||||
theme.
|
theme.
|
||||||
@@ -28,6 +37,34 @@ User templates in `~/.config/omarchy/themed/*.tpl` are processed before the
|
|||||||
built-in templates. If a user template has the same output filename as a
|
built-in templates. If a user template has the same output filename as a
|
||||||
built-in template, the built-in output is skipped.
|
built-in template, the built-in output is skipped.
|
||||||
|
|
||||||
|
After activation, `omarchy-theme-set` fires the `theme-set` hook
|
||||||
|
(`~/.config/omarchy/hooks/theme-set*`, theme name in `$1`) and dispatches a
|
||||||
|
parallel retint of running apps — terminals, Hyprland, btop, browser, editors,
|
||||||
|
and the rest of the `post_theme_commands` list in `bin/omarchy-theme-set`.
|
||||||
|
Making a new app follow theme changes means adding its restart/retint command
|
||||||
|
to that list. Runs serialize on a `flock`, so scripted theme changes queue
|
||||||
|
instead of racing.
|
||||||
|
|
||||||
|
## What an installed theme may not ship
|
||||||
|
|
||||||
|
`themes/<name>/` in this repo is Omarchy's own code and is trusted. So is a theme the user wrote by hand in `~/.config/omarchy/themes/<name>/`: it is their machine and their file, and both stage in full.
|
||||||
|
|
||||||
|
`omarchy theme install <url>` is different. It clones a stranger's git repo straight into that same directory, so the contents are whatever the theme author pushed. `omarchy-theme-set` tells the two apart the way `omarchy-theme-extras` already does — a `.git` directory means it was cloned, while a plain directory or a symlink to a working copy is the user's own — and from a cloned one it drops only what can run code:
|
||||||
|
|
||||||
|
- any `*.lua` — Hyprland `require`s a theme's `hyprland.lua` and `gum_env.lua` at login, and Neovim loads its `neovim.lua` at startup
|
||||||
|
- `alacritty.toml`, `foot.ini`, `ghostty.conf`, `kitty.conf` — each names the program the terminal launches
|
||||||
|
- `vscode.json` — names the extension `omarchy-theme-set-vscode` installs, and a VS Code extension is arbitrary JavaScript
|
||||||
|
|
||||||
|
Symlinks are dropped with them, at any depth; in a cloned theme they point wherever the theme author chose. Everything a cloned theme ships that is colour is kept, including files Omarchy would otherwise have generated — `btop.theme`, `chromium.theme`, `helix.toml`, `shell.toml`, `icons.theme`, `keyboard.rgb` and the rest — so a theme can still say exactly how it wants each app to look. What is dropped gets generated from `default/themed/*.tpl` instead, and is named on stderr.
|
||||||
|
|
||||||
|
A denylist is only right while it is maintained. Adding a template for another terminal, or for another editor that loads Lua, means adding it to `INSTALLED_THEME_DENIED` in `bin/omarchy-theme-set`; `test/shell.d/theme-staging-test.sh` fails on any `default/themed/*.tpl` whose output is recorded as neither code nor colour, so a new template cannot be added without that decision being made.
|
||||||
|
|
||||||
|
A theme predating `colors.toml` is not left without a palette: its `alacritty.toml` is read through `omarchy-theme-colors-from-alacritty` into a scratch directory and only the resulting `colors.toml` is staged, so the colors survive and the terminal config does not.
|
||||||
|
|
||||||
|
The restriction lives in `omarchy-theme-set` rather than in `omarchy-theme-install` on purpose. Filtering at staging also covers themes installed before the rule existed and files a theme gains later through `omarchy theme update`.
|
||||||
|
|
||||||
|
What this does not cover: a theme distributed as an archive rather than a git repo, extracted into `~/.config/omarchy/themes/` by hand, is indistinguishable from one the user wrote and stages in full. `omarchy theme install` only takes git URLs, so the supported path is always filtered, but the check is a statement about where a theme came from and not a sandbox.
|
||||||
|
|
||||||
## `colors.toml`
|
## `colors.toml`
|
||||||
|
|
||||||
`colors.toml` provides the palette keys used by templates. Keys are grouped
|
`colors.toml` provides the palette keys used by templates. Keys are grouped
|
||||||
@@ -335,6 +372,7 @@ local active_border_color = { colors = { "rgba(33ccffee)", "rgba(00ff99ee)" }, a
|
|||||||
## Adding or overriding theme files
|
## Adding or overriding theme files
|
||||||
|
|
||||||
- Add palette values to `themes/<name>/colors.toml`.
|
- Add palette values to `themes/<name>/colors.toml`.
|
||||||
|
- Hand-written overrides work everywhere except a `.lua`, a terminal config or a `vscode.json` in a theme cloned from a git repo; see [What an installed theme may not ship](#what-an-installed-theme-may-not-ship).
|
||||||
- Prefer generated files when the theme can be expressed with templates.
|
- Prefer generated files when the theme can be expressed with templates.
|
||||||
- Add a hand-written file in `themes/<name>/` only when that theme needs to
|
- Add a hand-written file in `themes/<name>/` only when that theme needs to
|
||||||
override the generated output entirely.
|
override the generated output entirely.
|
||||||
|
|||||||
@@ -1,5 +1,45 @@
|
|||||||
HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs)
|
HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs)
|
||||||
|
|
||||||
|
# The proprietary NVIDIA driver does early KMS itself: nvidia.conf (written by
|
||||||
|
# install/hardware/nvidia.sh, sourced before this file) early-loads nvidia_drm
|
||||||
|
# with modeset=1. Keeping the kms hook on such a system makes autodetect pull
|
||||||
|
# in nouveau — and ~100 MB of its GSP firmware — for a driver that never runs.
|
||||||
|
# Drop kms only when nvidia_drm is early-loaded and NVIDIA owns every display
|
||||||
|
# controller. Hybrid systems keep kms: their iGPU still needs it for early
|
||||||
|
# KMS at the LUKS prompt. So does anything unexpected, like a PCI tree that
|
||||||
|
# cannot be read.
|
||||||
|
#
|
||||||
|
# This reads MODULES midway through mkinitcpio's drop-in sourcing, so a
|
||||||
|
# later-sorting drop-in that resets MODULES outright — surface_device_modules.conf
|
||||||
|
# does — would strip nvidia_drm after kms was already dropped. Every machine
|
||||||
|
# Omarchy writes such a file for carries an Intel iGPU, which keeps kms here
|
||||||
|
# through the scan below; keep it that way.
|
||||||
|
if [[ " ${MODULES[*]:-} " == *" nvidia_drm "* ]]; then
|
||||||
|
_omarchy_nvidia_gpu=0
|
||||||
|
_omarchy_other_gpu=0
|
||||||
|
for _omarchy_pci in "${OMARCHY_PCI_DEVICES_PATH:-/sys/bus/pci/devices}"/*; do
|
||||||
|
if [[ ! -r $_omarchy_pci/class || ! -r $_omarchy_pci/vendor ]]; then
|
||||||
|
# An unreadable device could be another GPU. Inconclusive keeps kms.
|
||||||
|
_omarchy_other_gpu=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[[ $(<"$_omarchy_pci/class") == "0x03"* ]] || continue
|
||||||
|
if [[ $(<"$_omarchy_pci/vendor") == "0x10de" ]]; then
|
||||||
|
_omarchy_nvidia_gpu=1
|
||||||
|
else
|
||||||
|
_omarchy_other_gpu=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if ((_omarchy_nvidia_gpu && !_omarchy_other_gpu)); then
|
||||||
|
_omarchy_hooks=()
|
||||||
|
for _omarchy_hook in "${HOOKS[@]}"; do
|
||||||
|
[[ $_omarchy_hook == "kms" ]] || _omarchy_hooks+=("$_omarchy_hook")
|
||||||
|
done
|
||||||
|
HOOKS=("${_omarchy_hooks[@]}")
|
||||||
|
fi
|
||||||
|
unset _omarchy_nvidia_gpu _omarchy_other_gpu _omarchy_pci _omarchy_hooks _omarchy_hook
|
||||||
|
fi
|
||||||
|
|
||||||
# Bundle vconsole.conf so Plymouth uses the configured keyboard layout at the
|
# Bundle vconsole.conf so Plymouth uses the configured keyboard layout at the
|
||||||
# LUKS prompt, but only when that layout types Latin letters. Passphrases are
|
# LUKS prompt, but only when that layout types Latin letters. Passphrases are
|
||||||
# Latin characters, so bundling a Hebrew/Greek/Cyrillic/Arabic layout would
|
# Latin characters, so bundling a Hebrew/Greek/Cyrillic/Arabic layout would
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Switching between the stock providers is a one-click toggle in the network
|
||||||
|
# panel, so it must not stop for a password. Custom is deliberately absent: it
|
||||||
|
# points the machine at servers the caller supplies, and it already runs in a
|
||||||
|
# terminal that can ask.
|
||||||
|
%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-dns Cloudflare, /usr/bin/omarchy-dns Google, /usr/bin/omarchy-dns DHCP
|
||||||
+16
-10
@@ -1,10 +1,16 @@
|
|||||||
# Record the docker group for provisioning first-boot user creation and factory reset,
|
# The Docker daemon runs as root and its socket is root-owned, so membership in
|
||||||
# then grant it directly when the install user already exists (deferred-provisioning
|
# the docker group is equivalent to passwordless root: any process in it can
|
||||||
# installs create the user at first boot instead).
|
# `docker run -v /:/host` and rewrite the host as root. We therefore do NOT add
|
||||||
provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}"
|
# the install user to the docker group by default, so a single rogue process
|
||||||
mkdir -p "$provisioning_dir"
|
# running as the user cannot silently escalate to root.
|
||||||
grep -qxF docker "$provisioning_dir/groups" 2>/dev/null || echo docker >>"$provisioning_dir/groups"
|
#
|
||||||
|
# The daemon is still enabled (docker.socket, in enable-services.sh) for system
|
||||||
if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then
|
# use. The Docker TUI (Super + Shift + D) and the Windows VM reach it through a
|
||||||
usermod -aG docker "$OMARCHY_INSTALL_USER"
|
# polkit prompt, and the plain `docker` CLI runs under sudo. Users who want the
|
||||||
fi
|
# convenience back can opt in, behind a warning, with:
|
||||||
|
#
|
||||||
|
# omarchy-setup-security-sudoless-docker (Setup > Security > Sudoless Docker)
|
||||||
|
#
|
||||||
|
# Nothing to do here now that the group is no longer granted, but the file stays
|
||||||
|
# as the recorded home of this decision and a hook for future daemon config.
|
||||||
|
:
|
||||||
@@ -1,6 +1,25 @@
|
|||||||
# Enable Synaptics InterTouch for confirmed touchpads if not already loaded
|
# Enable Synaptics InterTouch for confirmed touchpads if not already loaded
|
||||||
|
#
|
||||||
if grep -qi synaptics /proc/bus/input/devices \
|
# Only when modprobe can resolve psmouse for the running kernel, and never
|
||||||
&& ! lsmod | grep -q '^psmouse'; then
|
# fatally. Installs run this under arch-chroot, where uname -r still names the
|
||||||
modprobe psmouse synaptics_intertouch=1
|
# live ISO's kernel while /lib/modules holds the target's -- the two differ on
|
||||||
|
# every machine that is not a T2 Mac, so modprobe failed with "Module psmouse
|
||||||
|
# not found in directory /lib/modules/<live kernel>" and took the whole install
|
||||||
|
# down with it. Asking modprobe itself first (-qn resolves a module without
|
||||||
|
# loading it) keeps that mismatch quiet, and the warning below covers every
|
||||||
|
# other reason it declines. An optional touchpad improvement must not be able to
|
||||||
|
# halt an install.
|
||||||
|
#
|
||||||
|
# Loading a module into the live kernel does nothing for the installed system
|
||||||
|
# either way, so this only takes effect when it runs on the booted machine.
|
||||||
|
# Persisting the switch instead would mean writing options psmouse
|
||||||
|
# synaptics_intertouch=1 to /etc/modprobe.d, which forces the SMBus transport
|
||||||
|
# past the kernel's own allowlist on every touchpad merely named "synaptics" in
|
||||||
|
# /proc/bus/input/devices -- a wider change than this one, and not one to make
|
||||||
|
# blind.
|
||||||
|
if grep -qi synaptics "${OMARCHY_SYNAPTIC_INPUT_DEVICES:-/proc/bus/input/devices}" \
|
||||||
|
&& ! lsmod | grep -q '^psmouse' \
|
||||||
|
&& modprobe -qn psmouse; then
|
||||||
|
modprobe psmouse synaptics_intertouch=1 ||
|
||||||
|
echo "Warning: could not enable Synaptics InterTouch on psmouse" >&2
|
||||||
fi
|
fi
|
||||||
@@ -78,7 +78,7 @@ lua51
|
|||||||
luarocks
|
luarocks
|
||||||
man-db
|
man-db
|
||||||
mariadb-libs
|
mariadb-libs
|
||||||
mise
|
mise-bin
|
||||||
moonlight-qt
|
moonlight-qt
|
||||||
mpv
|
mpv
|
||||||
mpv-mpris
|
mpv-mpris
|
||||||
@@ -108,7 +108,8 @@ python-poetry-core
|
|||||||
ttfx
|
ttfx
|
||||||
qemu-user-static-binfmt
|
qemu-user-static-binfmt
|
||||||
qrencode
|
qrencode
|
||||||
quickshell-git
|
qt6-imageformats
|
||||||
|
quickshell
|
||||||
ripgrep
|
ripgrep
|
||||||
ruby
|
ruby
|
||||||
tensaku
|
tensaku
|
||||||
|
|||||||
Loaded 100 of 329 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user