Merge remote-tracking branch 'origin/quattro' into fix-snapshot-locate-and-timeline-leak

# Conflicts:
#	docs/file-layout.md
This commit is contained in:
David Heinemeier Hansson
2026-07-26 19:14:09 -07:00
240 changed files with 6263 additions and 2472 deletions
+1 -1
View File
@@ -262,7 +262,7 @@ This copies `$OMARCHY_PATH/config/hypr/hyprlock.conf` to `~/.config/hypr/hyprloc
Read `docs/migrations.md` before creating or changing migrations.
Migrations are per-user and run through `omarchy-migrate` during `omarchy update` or from the migration notification. Put migrations directly under `migrations/<timestamp>.sh`. Pending state is per-user under `~/.local/state/omarchy/migrations/`, so every user gets a chance to run every migration. Migrations run as the user; privileged work should invoke the appropriate helper or privilege prompt, and no-op when another user already applied it.
Migrations are per-user and run through `omarchy-migrate` during `omarchy update` or from the login-time migration notification. Put migrations directly under `migrations/<timestamp>.sh`. Pending state is per-user under `~/.local/state/omarchy/migrations/`, so every user gets a chance to run every migration. Migrations run as the user; privileged work should invoke the appropriate helper or privilege prompt, and no-op when another user already applied it.
To create a new migration, run `omarchy-dev-add-migration --no-edit`.
+26 -39
View File
@@ -79,6 +79,7 @@ GROUP_DESCRIPTIONS[style]="Global UI style controls"
GROUP_DESCRIPTIONS[sudo]="Sudo configuration helpers"
GROUP_DESCRIPTIONS[system]="System status, reboot, shutdown, logout, and lock"
GROUP_DESCRIPTIONS[theme]="Theme management"
GROUP_DESCRIPTIONS[tmux]="Tmux session helpers"
GROUP_DESCRIPTIONS[toggle]="Toggle Omarchy features"
GROUP_DESCRIPTIONS[transcode]="Image and video transcoding"
GROUP_DESCRIPTIONS[tui]="Terminal UI launchers"
@@ -143,7 +144,6 @@ register_command() {
local name=""
local summary=""
local usage=""
local binary=""
local args=""
local examples=""
local aliases=""
@@ -233,7 +233,6 @@ register_command() {
fallback_name="${fallback_name//-/ }"
fi
[[ -z $binary ]] && binary="$file_binary"
[[ -z $group ]] && group="$fallback_group"
[[ $name_seen != "true" ]] && name="$fallback_name"
[[ -z $summary && -n $fallback_summary ]] && summary="$fallback_summary"
@@ -243,11 +242,9 @@ register_command() {
route+=" $name"
fi
if [[ -z $usage ]]; then
usage="$route"
if [[ -n $args ]]; then
usage+=" $args"
fi
usage="$route"
if [[ -n $args ]]; then
usage+=" $args"
fi
[[ $requires_sudo == "true" ]] || requires_sudo="false"
@@ -257,7 +254,7 @@ register_command() {
COMMAND_KEYS+=("$key")
COMMAND_ROUTE["$key"]="$route"
COMMAND_FALLBACK_ROUTE["$key"]="$fallback_route"
COMMAND_BINARY["$key"]="$binary"
COMMAND_BINARY["$key"]="$file_binary"
COMMAND_GROUP["$key"]="$group"
COMMAND_NAME["$key"]="$name"
COMMAND_SUMMARY["$key"]="$summary"
@@ -269,7 +266,7 @@ register_command() {
COMMAND_HAS_SUMMARY["$key"]="$has_summary"
COMMAND_METADATA_ERRORS["$key"]="$metadata_errors"
BINARY_TO_KEY["$binary"]="$key"
BINARY_TO_KEY["$file_binary"]="$key"
register_route "$route" "$key"
register_route "$fallback_route" "$key"
@@ -615,25 +612,31 @@ show_commands_markdown() {
done < <(sorted_keys "$include_all")
}
emit_command_record() {
local key="$1"
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"${COMMAND_ROUTE[$key]}" \
"${COMMAND_BINARY[$key]}" \
"${COMMAND_GROUP[$key]}" \
"${COMMAND_NAME[$key]}" \
"${COMMAND_SUMMARY[$key]}" \
"${COMMAND_REQUIRES_SUDO[$key]}" \
"${COMMAND_HIDDEN[$key]}" \
"${COMMAND_ARGS[$key]}" \
"${COMMAND_EXAMPLES[$key]}" \
"${COMMAND_ALIASES[$key]}" \
"${COMMAND_FALLBACK_ROUTE[$key]}" \
"${COMMAND_USAGE[$key]}"
}
emit_command_records() {
local include_all="$1"
local key=""
while IFS= read -r key; do
[[ -n $key ]] || continue
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"${COMMAND_ROUTE[$key]}" \
"${COMMAND_BINARY[$key]}" \
"${COMMAND_GROUP[$key]}" \
"${COMMAND_NAME[$key]}" \
"${COMMAND_SUMMARY[$key]}" \
"${COMMAND_REQUIRES_SUDO[$key]}" \
"${COMMAND_HIDDEN[$key]}" \
"${COMMAND_ARGS[$key]}" \
"${COMMAND_EXAMPLES[$key]}" \
"${COMMAND_ALIASES[$key]}" \
"${COMMAND_FALLBACK_ROUTE[$key]}" \
"${COMMAND_USAGE[$key]}"
emit_command_record "$key"
done < <(sorted_keys "$include_all")
}
@@ -702,23 +705,7 @@ show_commands_check() {
}
show_command_json() {
local key="$1"
printf '%s\n' "$key" | while IFS= read -r key; do
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"${COMMAND_ROUTE[$key]}" \
"${COMMAND_BINARY[$key]}" \
"${COMMAND_GROUP[$key]}" \
"${COMMAND_NAME[$key]}" \
"${COMMAND_SUMMARY[$key]}" \
"${COMMAND_REQUIRES_SUDO[$key]}" \
"${COMMAND_HIDDEN[$key]}" \
"${COMMAND_ARGS[$key]}" \
"${COMMAND_EXAMPLES[$key]}" \
"${COMMAND_ALIASES[$key]}" \
"${COMMAND_FALLBACK_ROUTE[$key]}" \
"${COMMAND_USAGE[$key]}"
done | jq -Rn "$(commands_json_filter) | {ok: true, command: .commands[0]}"
emit_command_record "$1" | jq -Rn "$(commands_json_filter) | {ok: true, command: .commands[0]}"
}
parse_commands_args() {
+12 -1
View File
@@ -15,6 +15,17 @@ fi
timeout 2 wpctl set-default "$node_id" 2>/dev/null || true
timeout 2 pactl set-default-sink "$sink_name" 2>/dev/null || true
timeout 2 pactl list short sink-inputs 2>/dev/null | awk '{ print $1 }' | while read -r input; do
# Move only real application streams. A DSP filter-chain's own output is also a
# sink input but carries no application.name, and moving it would rewire the
# processing itself -- onto headphones, or into its own virtual sink, which is a
# cycle. EasyEffects' output stream must stay put for the same reason.
timeout 2 pactl list sink-inputs 2>/dev/null | awk '
/^Sink Input #/ {id = substr($3, 2)}
/application\.name = / {
app = $0
sub(/.*application\.name = "/, "", app)
sub(/"$/, "", app)
if (app != "EasyEffects") print id
}' | while read -r input; do
[[ -n $input ]] && timeout 2 pactl move-sink-input "$input" "$sink_name" 2>/dev/null || true
done
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# omarchy:summary=Print the sink whose volume and mute a given output really uses
# omarchy:args=[sink-name]
# omarchy:group=audio
# omarchy:examples=omarchy audio output sink | omarchy audio output sink omarchy_speaker_tuning
set -uo pipefail
# A DSP sink -- a speaker tuning filter-chain, or EasyEffects -- can be the
# selected output without being where loudness lives. Changing its volume alters
# the level going *into* the processing: the display moves while the speakers do
# not, and on a chain with a compressor or limiter the tone changes too. Resolve
# through it to the physical sink it feeds.
#
# With no argument this resolves the current default output, so when headphones or
# HDMI are selected it returns those, not the speakers a tuning happens to front.
# Callers that need to describe some *other* output -- an output switcher naming
# the next one in the rotation -- pass that sink explicitly.
sink="${1:-$(pactl get-default-sink 2>/dev/null)}"
if [[ -z $sink || $sink == alsa_output.* ]]; then
printf '%s\n' "$sink"
exit 0
fi
# A DSP sink feeds its physical output through a stream of its own; follow that
# stream down to the sink underneath.
downstream="$(pactl list sink-inputs 2>/dev/null |
awk -v virt="$sink" '
/^Sink Input #/ {target = ""}
/^[[:space:]]*Sink:/ {target = $2}
/node\.name = / {
name = $0
sub(/.*node\.name = "/, "", name)
sub(/"$/, "", name)
if (index(name, virt) == 1 && target != "") {print target; exit}
}
/application\.name = "EasyEffects"/ {
if (virt == "easyeffects_sink" && target != "") {print target; exit}
}')"
if [[ -n $downstream ]]; then
name="$(pactl list sinks short 2>/dev/null |
awk -v id="$downstream" '$1 == id {print $2; exit}')"
if [[ -n $name ]]; then
printf '%s\n' "$name"
exit 0
fi
fi
# Nothing resolvable downstream -- the DSP sink may simply be idle and unlinked.
# Fall back to the sink itself so callers still have something to act on.
printf '%s\n' "$sink"
+20 -3
View File
@@ -2,7 +2,14 @@
# omarchy:summary=Switch between audio outputs while preserving the mute status
sinks=$(timeout 2 pactl -f json list sinks | jq '[.[] | select((.ports | length == 0) or ([.ports[]? | .availability != "not available"] | any))]')
# Skip the physical sink an active speaker tuning fronts: rotating onto it would
# silently bypass the tuning rather than pick a different output.
fronted=$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)
sinks=$(timeout 2 pactl -f json list sinks |
jq --arg fronted "$fronted" '[.[]
| select((.ports | length == 0) or ([.ports[]? | .availability != "not available"] | any))
| select($fronted == "" or .name != $fronted)]')
sinks_count=$(jq 'length' <<<"$sinks")
if (( sinks_count == 0 )); then
@@ -22,8 +29,18 @@ fi
next_sink=$(jq -c ".[$next_sink_index]" <<<"$sinks")
next_sink_name=$(jq -r '.name' <<<"$next_sink")
next_sink_description=$(jq -r '.description // .properties."device.description" // .name' <<<"$next_sink")
next_sink_volume=$(jq -r '.volume | to_entries[0].value.value_percent | sub("%"; "") | tonumber' <<<"$next_sink")
next_sink_is_muted=$(jq -r '.mute' <<<"$next_sink")
# A tuning sink sits at a fixed 100% and unmuted while real loudness lives on the
# physical sink beneath it, so read the level from whichever sink actually carries
# it or the OSD contradicts the volume keys.
next_sink_effective=$(omarchy-audio-output-sink "$next_sink_name")
next_sink_volume=$(timeout 2 pactl get-sink-volume "$next_sink_effective" 2>/dev/null |
awk 'NR == 1 {for (i = 1; i <= NF; i++) if ($i ~ /%$/) {sub("%", "", $i); print $i; exit}}')
[[ -n $next_sink_volume ]] || next_sink_volume=$(jq -r '.volume | to_entries[0].value.value_percent | sub("%"; "") | tonumber' <<<"$next_sink")
if [[ $(timeout 2 pactl get-sink-mute "$next_sink_effective" 2>/dev/null) == *yes ]]; then
next_sink_is_muted=true
else
next_sink_is_muted=false
fi
if [[ $next_sink_is_muted == "true" ]] || (( next_sink_volume == 0 )); then
icon_state="muted"
+42 -23
View File
@@ -11,20 +11,27 @@ if [[ -z $action ]]; then
exit 1
fi
volume_state() {
wpctl get-volume @DEFAULT_AUDIO_SINK@
}
# Resolve through any DSP sink to the physical one, so the keys always move real
# loudness and the processing always sees full-scale input. Shared with the audio
# panel and the output switcher.
sink="$(omarchy-audio-output-sink)"
if [[ -z $sink ]]; then
echo "Could not resolve an audio sink to control." >&2
exit 1
fi
# pactl reports the same percentage scale wpctl does (both are the raw volume
# over PA_VOLUME_NORM), so the OSD reads identically either way.
volume_percent() {
volume_state | awk '{ for (i=1; i<=NF; i++) if ($i ~ /^[0-9.]+$/) print int($i * 100) }'
pactl get-sink-volume "$sink" 2>/dev/null |
awk 'NR == 1 {
for (i = 1; i <= NF; i++)
if ($i ~ /%$/) {sub("%", "", $i); print $i; exit}
}'
}
volume_muted() {
volume_state | grep -q MUTED
}
unmute_output() {
wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 >/dev/null
[[ $(pactl get-sink-mute "$sink" 2>/dev/null) == *yes ]]
}
case "$action" in
@@ -37,31 +44,43 @@ if [[ $action == "mute-toggle" ]]; then
debounce_file="$runtime_dir/omarchy-audio-output-volume-mute-toggle.last"
now=$(date +%s%3N)
last=0
[[ -r $debounce_file ]] && read -r last < "$debounce_file" || true
if (( now - last < 250 )); then
[[ -r $debounce_file ]] && read -r last <"$debounce_file" || true
if ((now - last < 250)); then
exit 0
fi
printf '%s\n' "$now" > "$debounce_file"
printf '%s\n' "$now" >"$debounce_file"
wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle >/dev/null
elif [[ $action == +* ]]; then
step="${action#+}"
unmute_output
wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ "${step}%+"
elif [[ $action == -* ]]; then
step="${action#-}"
unmute_output
wpctl set-volume @DEFAULT_AUDIO_SINK@ "${step}%-"
pactl set-sink-mute "$sink" toggle
elif [[ $action =~ ^([+-])([0-9]+)$ ]]; then
direction="${BASH_REMATCH[1]}"
step="${BASH_REMATCH[2]}"
current="$(volume_percent)"
if [[ -z $current ]]; then
echo "Could not read volume for $sink." >&2
exit 1
fi
if [[ $direction == "+" ]]; then
next=$((current + step))
((next <= 100)) || next=100
else
next=$((current - step))
((next >= 0)) || next=0
fi
pactl set-sink-mute "$sink" 0
pactl set-sink-volume "$sink" "${next}%"
else
echo "Unknown volume action: $action"
exit 1
fi
percent=$(volume_percent)
if volume_muted || (( percent == 0 )); then
if volume_muted || ((${percent:-0} == 0)); then
icon="volume-muted"
else
icon="volume-high"
fi
omarchy-osd -i "$icon" -p "$percent"
omarchy-osd -i "$icon" -p "${percent:-0}"
+10 -1
View File
@@ -3,9 +3,18 @@
# omarchy:summary=Print PulseAudio sink availability for the shell
# omarchy:group=audio
pactl list sinks 2>/dev/null | awk '
# A speaker tuning is a virtual sink in front of the real speakers. Both exist
# in the graph, but selecting the physical one would only bypass the tuning, so
# report it unavailable and keep it out of the output list.
fronted="$(omarchy-audio-tuning fronted-sink 2>/dev/null || true)"
pactl list sinks 2>/dev/null | awk -v fronted="$fronted" '
function emit_sink() {
if (name == "") return
if (fronted != "" && name == fronted) {
print name "\t0"
return
}
print name "\t" ((port_count == 0 || available) ? 1 : 0)
}
+357
View File
@@ -0,0 +1,357 @@
#!/bin/bash
# omarchy:summary=Manage the speaker tuning for this laptop
# omarchy:args=<on|off|status|match|fronted-sink> [--force]
# omarchy:group=audio
# omarchy:examples=omarchy audio tuning status | omarchy audio tuning on | omarchy audio tuning off
set -uo pipefail
tunings_dir="$OMARCHY_PATH/default/audio/tunings"
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
# The tuning is hosted by its own PipeWire client, under its own config name, so
# switching it needs no audio restart -- a restart drops every PulseAudio client's
# connection, and applications that do not reconnect (Spotify) then have to be
# restarted by hand. The name is deliberately not PipeWire's stock
# filter-chain.conf, which merges every fragment in filter-chain.conf.d/ and would
# make this service host unrelated user filters too.
host_config_name=omarchy-speaker-tuning.conf
host_config="$config_home/pipewire/$host_config_name"
host_source="$OMARCHY_PATH/default/audio/filter-chain-host.conf"
fragment="$config_home/pipewire/$host_config_name.d/90-tuning.conf"
unit_name=omarchy-speaker-tuning.service
unit="$config_home/systemd/user/$unit_name"
unit_source="$OMARCHY_PATH/default/systemd/user/$unit_name"
# Earlier revisions loaded the tuning into the daemon, as a WirePlumber smart
# filter, or into the shared filter-chain.conf.d namespace. Remove all three so
# they cannot be loaded alongside the current one.
stale_daemon="$config_home/pipewire/pipewire.conf.d/90-omarchy-speaker-tuning.conf"
stale_wireplumber="$config_home/wireplumber/wireplumber.conf.d/90-omarchy-speaker-tuning.conf"
stale_shared="$config_home/pipewire/filter-chain.conf.d/90-omarchy-speaker-tuning.conf"
sink_name=omarchy_speaker_tuning
action="${1:-status}"
force=0
[[ ${2:-} == "--force" ]] && force=1
sink_matching() {
pactl list sinks short 2>/dev/null | awk -v p="$1" '$2 ~ p {print $2; exit}'
}
# Dell keys its Cirrus speaker firmware on the DMI product SKU, which makes it the
# most precise identifier available for these machines -- narrower than a product
# name, and it distinguishes models whose names differ only by marketing. Compared
# case-insensitively against an exact SKU, never a substring, so a tuning cannot
# accidentally widen to a whole product line.
sku_matches() {
local sku want
sku="$(cat /sys/class/dmi/id/product_sku 2>/dev/null)"
[[ -n $sku ]] || return 1
for want in "$@"; do
[[ ${sku,,} == "${want,,}" ]] && return 0
done
return 1
}
dmi_matches() {
local want
for want in "$@"; do
omarchy-hw-match "$want" 2>/dev/null && return 0
done
return 1
}
# Print the tuning directory matching this laptop, if any. Matching is data, not
# code: a tuning declares the DMI string it belongs to and the sink it expects, so
# most tunings can be added as a directory with no new script. A tuning whose
# hardware needs a sharper test can set match_command to any predicate instead.
tuning_match() {
local dir
for dir in "$tunings_dir"/*/; do
[[ -r $dir/tuning.conf ]] || continue
unset match_dmi match_sku match_command sink_pattern
# shellcheck disable=SC1090
source "$dir/tuning.conf"
# Deliberately does not look at the live audio graph. The install hooks run in
# the ISO chroot with no audio server, and a match that depended on a present
# sink would come back empty there -- so the machine would get neither the LV2
# dependency nor the tuning, and nothing would retry.
# A tuning may list several models it has been validated on. match_dmi and
# match_sku are arrays, so a plain string still works as a single entry.
if [[ -n ${match_command:-} ]]; then
"$match_command" 2>/dev/null || continue
elif [[ -n ${match_sku:-} ]]; then
sku_matches "${match_sku[@]}" || continue
elif [[ -n ${match_dmi:-} ]]; then
dmi_matches "${match_dmi[@]}" || continue
else
continue
fi
# Required whichever way the tuning matched: the graph's target sink is
# substituted from it, so a tuning without one cannot be installed and must
# not be reported as a match.
[[ -n ${sink_pattern:-} ]] || continue
printf '%s\n' "${dir%/}"
return 0
done
return 1
}
# The physical sink the matched tuning is built for, taken from the tuning's own
# sink_pattern rather than a hard-coded regex, so hardware with a different sink
# name needs no change here.
tuned_hardware_sink() {
local dir found
dir="$(tuning_match)" || return 1
unset sink_pattern
# shellcheck disable=SC1090
source "$dir/tuning.conf"
[[ -n ${sink_pattern:-} ]] || return 1
found="$(sink_matching "$sink_pattern")"
[[ -n $found ]] || return 1
printf '%s\n' "$found"
}
tuning_present() {
pactl list sinks short 2>/dev/null | awk '{print $2}' | grep -qx "$sink_name"
}
# Only real application streams may be moved. A filter-chain's own output is also
# a sink input but carries no application.name, and moving it would rewire the
# tuning itself.
app_streams() {
pactl list sink-inputs 2>/dev/null | awk '
/^Sink Input #/ {id = substr($3, 2)}
/application\.name = / {
app = $0
sub(/.*application\.name = "/, "", app)
sub(/"$/, "", app)
if (app != "EasyEffects") print id
}'
}
move_apps_to() {
local target="$1" id
for id in $(app_streams); do
pactl move-sink-input "$id" "$target" 2>/dev/null || true
done
}
# WirePlumber can link the output elsewhere if the target is missing when the host
# starts. node.dont-fallback guards against it, but verify rather than assume.
tuning_downstream_sink() {
omarchy-audio-output-sink "$sink_name" 2>/dev/null
}
easyeffects_running() {
pactl list sinks short 2>/dev/null | awk '{print $2}' | grep -qx easyeffects_sink ||
pgrep -u "$(id -u)" -x easyeffects >/dev/null 2>&1 ||
systemctl --user is-active --quiet easyeffects.service 2>/dev/null
}
# Unloading a daemon-loaded drop-in is the one case that still needs an audio
# restart, because the daemon only reads its own config at startup.
drop_stale_daemon_config() {
[[ -e $stale_daemon || -e $stale_wireplumber ]] || return 0
rm -f "$stale_daemon" "$stale_wireplumber"
omarchy-restart-audio >/dev/null 2>&1
local _
for _ in {1..40}; do
pactl info >/dev/null 2>&1 && break
sleep 0.25
done
}
case "$action" in
match)
tuning_match
;;
fronted-sink)
# The tuning is a virtual sink in front of the real speakers, so both exist in
# the graph. Selecting the physical one would only bypass the tuning, so
# callers keep it out of the output list while the tuning is up. This answers
# "is a tuning in place", not "where should volume go" -- for the latter see
# omarchy-audio-output-sink, which follows the current default output.
tuning_present || exit 1
tuned_hardware_sink
;;
status)
if [[ -r $fragment ]]; then
echo "Installed: yes ($fragment)"
else
echo "Installed: no"
fi
# Both is-active and is-enabled print their answer *and* exit non-zero when
# negative, so a "|| echo" fallback prints it twice.
host_state="$(systemctl --user is-active "$unit_name" 2>/dev/null)"
host_enabled="$(systemctl --user is-enabled "$unit_name" 2>/dev/null)"
echo "Host service: ${host_state:-inactive} (${host_enabled:-disabled})"
if tuning_present; then
echo "Tuning sink: present"
else
echo "Tuning sink: absent"
fi
echo "Default sink: $(pactl get-default-sink 2>/dev/null)"
if dir="$(tuning_match)"; then
unset description
# shellcheck disable=SC1090
source "$dir/tuning.conf"
echo "Matches: ${description:-?} ($(basename "$dir"))"
else
echo "Matches: nothing ships for this laptop"
fi
;;
off)
if [[ ! -r $fragment && ! -r $unit && ! -r $stale_daemon && ! -r $stale_wireplumber &&
! -r $stale_shared ]]; then
echo "No speaker tuning installed."
exit 0
fi
speakers="$(tuned_hardware_sink)" || speakers=""
systemctl --user disable --now "$unit_name" >/dev/null 2>&1
rm -f "$fragment" "$host_config" "$unit" "$stale_shared"
rmdir "$config_home/pipewire/$host_config_name.d" 2>/dev/null
systemctl --user daemon-reload >/dev/null 2>&1
drop_stale_daemon_config
for _ in {1..20}; do
tuning_present || break
sleep 0.25
done
if [[ -n $speakers ]]; then
pactl set-default-sink "$speakers" >/dev/null 2>&1
# Streams left on the vanished tuning sink reconnect wherever PipeWire puts
# them, which is not necessarily the speakers.
move_apps_to "$speakers"
fi
echo "Speaker tuning removed."
;;
on)
[[ -d $tunings_dir ]] || {
echo "No tunings shipped at $tunings_dir" >&2
exit 1
}
selected="$(tuning_match)" || {
echo "No speaker tuning matches this laptop."
exit 0
}
unset description sink_pattern
# shellcheck disable=SC1090
source "$selected/tuning.conf"
# At first-run the session is up but the sink can still be settling.
for _ in {1..20}; do
speaker_sink="$(sink_matching "$sink_pattern")"
[[ -n $speaker_sink ]] && break
sleep 0.5
done
[[ -n ${speaker_sink:-} ]] || {
echo "A tuning applies to this laptop but no sink matching $sink_pattern" >&2
echo "is present, so there is no audio server yet. Re-run after login:" >&2
echo " omarchy audio tuning on" >&2
exit 1
}
if easyeffects_running; then
cat >&2 <<'EOF'
EasyEffects is running. It moves any stream that follows the default sink to its
own sink, so a tuning installed now would be bypassed.
Stop it first: systemctl --user disable --now easyeffects.service
EOF
exit 1
fi
# Every tuning ends in a limiter, which is an LV2 plugin. Without it the graph
# fails to instantiate and the tuning sink never appears.
ls /usr/lib/lv2/lsp-plugins.lv2/limiter_stereo.ttl >/dev/null 2>&1 || {
echo "lsp-plugins-lv2 is required for the tuning limiter." >&2
exit 1
}
rendered="$(mktemp)"
trap 'rm -f "$rendered"' EXIT
sed "s|@SPEAKER_SINK@|$speaker_sink|g" "$selected/filter-chain.conf" >"$rendered"
# Everything that makes the tuning current has to match, not just the graph:
# an active-but-disabled service disappears at next login, and a stale unit
# file would shadow later fixes to the shipped one indefinitely.
if ((!force)) && [[ -r $fragment ]] && cmp -s "$rendered" "$fragment" &&
[[ -r $host_config ]] && cmp -s "$host_source" "$host_config" &&
[[ -r $unit ]] && cmp -s "$unit_source" "$unit" &&
systemctl --user is-active --quiet "$unit_name" 2>/dev/null &&
systemctl --user is-enabled --quiet "$unit_name" 2>/dev/null &&
[[ "$(tuning_downstream_sink)" == "$speaker_sink" ]]; then
echo "Speaker tuning already current: $description"
exit 0
fi
drop_stale_daemon_config
rm -f "$stale_shared"
install -Dm644 "$host_source" "$host_config"
install -Dm644 "$rendered" "$fragment"
install -Dm644 "$unit_source" "$unit"
systemctl --user daemon-reload >/dev/null 2>&1
systemctl --user enable "$unit_name" >/dev/null 2>&1
systemctl --user restart "$unit_name" >/dev/null 2>&1
echo "Installed speaker tuning: $description"
for _ in {1..40}; do
tuning_present && break
sleep 0.25
done
if ! tuning_present; then
systemctl --user disable --now "$unit_name" >/dev/null 2>&1
rm -f "$fragment" "$host_config" "$unit"
systemctl --user daemon-reload >/dev/null 2>&1
echo "Tuning sink never appeared, so it was removed. Audio is untouched." >&2
echo "Check: systemctl --user status $unit_name" >&2
exit 1
fi
# Confirm the output really landed on the sink this tuning was measured for.
for _ in {1..20}; do
[[ "$(tuning_downstream_sink)" == "$speaker_sink" ]] && break
sleep 0.25
done
downstream="$(tuning_downstream_sink)"
if [[ $downstream != "$speaker_sink" ]]; then
systemctl --user disable --now "$unit_name" >/dev/null 2>&1
rm -f "$fragment" "$host_config" "$unit"
systemctl --user daemon-reload >/dev/null 2>&1
echo "The tuning output linked to ${downstream:-nothing} instead of" >&2
echo "$speaker_sink, so it was removed rather than left tuning the wrong" >&2
echo "device. Audio is untouched." >&2
exit 1
fi
pactl set-default-sink "$sink_name" >/dev/null 2>&1
# A default sink only captures newly created streams, so anything already
# playing would keep bypassing the tuning until its app was restarted.
move_apps_to "$sink_name"
echo "Speakers now play through the tuning."
;;
*)
echo "Usage: omarchy-audio-tuning <on|off|status|match|fronted-sink> [--force]" >&2
exit 2
;;
esac
+5 -61
View File
@@ -7,9 +7,7 @@
set -euo pipefail
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
OMARCHY_ROOT="${OMARCHY_PATH:-}"
DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json"
source omarchy-shell-config
usage() {
cat <<USAGE
@@ -32,60 +30,6 @@ Examples:
USAGE
}
fail() {
echo "omarchy-bar: $*" >&2
exit 1
}
refresh_shell_config() {
if ! omarchy-shell shell reloadConfig >/dev/null 2>&1; then
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
fi
}
source_file() {
if [[ -s $CONFIG_FILE ]]; then
printf '%s\n' "$CONFIG_FILE"
else
printf '%s\n' "$DEFAULTS_FILE"
fi
}
# jq pipeline that normalizes shell.json into a well-shaped object with
# version=1, bar.layout.{left,center,right} arrays, and plugins array.
NORMALIZE='
def object_or_empty: if type == "object" then . else {} end;
def array_or_empty: if type == "array" then . else [] end;
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
| .bar.layout = (.bar.layout | object_or_empty)
| .bar.layout.left = (.bar.layout.left | array_or_empty)
| .bar.layout.center = (.bar.layout.center | array_or_empty)
| .bar.layout.right = (.bar.layout.right | array_or_empty)
| .plugins = (.plugins | array_or_empty)
'
# Apply a jq program to the source file and atomically write the result to the
# user config, then refresh the running shell. Extra args after the program are
# forwarded to jq (e.g. --arg/--argjson).
_BAR_TMP=""
cleanup_bar_tmp() {
if [[ -n ${_BAR_TMP:-} ]]; then rm -f "$_BAR_TMP"; fi
}
trap cleanup_bar_tmp EXIT
commit() {
local program="$1"
shift
mkdir -p "$(dirname "$CONFIG_FILE")"
_BAR_TMP=$(mktemp)
jq -S -e "$@" "$program" "$(source_file)" >"$_BAR_TMP" || fail "could not update shell config"
mv "$_BAR_TMP" "$CONFIG_FILE"
_BAR_TMP=""
refresh_shell_config
}
# ------------------------------------------------------------------ validation
bar_option_exists() {
@@ -99,7 +43,7 @@ bar_option_exists() {
cmd_use() {
local plugin="${1:-}"
[[ -n $plugin ]] || fail "bar option id is required"
[[ $# -eq 1 ]] || fail "use takes a single bar option id"
(( $# == 1 )) || fail "use takes a single bar option id"
if [[ $plugin == "default" || $plugin == "built-in" ]]; then
plugin="omarchy.bar"
fi
@@ -130,7 +74,7 @@ cmd_defaults() {
cmd_position() {
local position="${1:-}"
[[ -n $position ]] || fail "position is required"
[[ $# -eq 1 ]] || fail "position takes a single value"
(( $# == 1 )) || fail "position takes a single value"
[[ $position =~ ^(top|bottom|left|right)$ ]] || fail "position must be top, bottom, left, or right"
commit "$NORMALIZE | .bar.position = \$position" --arg position "$position"
echo "Bar position set to $position"
@@ -139,7 +83,7 @@ cmd_position() {
cmd_transparent() {
local transparent="${1:-}"
[[ -n $transparent ]] || fail "transparent is required"
[[ $# -eq 1 ]] || fail "transparent takes a single value"
(( $# == 1 )) || fail "transparent takes a single value"
[[ $transparent =~ ^(true|false|toggle)$ ]] || fail "transparent must be true, false, or toggle"
if [[ $transparent == "toggle" ]]; then
commit "$NORMALIZE | .bar.transparent = (.bar.transparent != true)"
@@ -153,7 +97,7 @@ cmd_transparent() {
# --------------------------------------------------------------------- dispatch
command="${1:-}"
[[ $# -gt 0 ]] && shift || true
(( $# > 0 )) && shift || true
case "$command" in
use)
+28 -66
View File
@@ -3,13 +3,11 @@
# omarchy:summary=Add, move, remove, and configure bar plugin widgets in the layout
# omarchy:group=bar
# omarchy:args=add <id> [placement] | move <id> [placement] | remove <id> [placement] | set <id> <key> <value> [--json] [placement] | replace <old-id> <new-id>
# omarchy:examples=omarchy bar plugin add omarchy.tailscale | omarchy bar plugin add omarchy.clock --section center --before omarchy.weather | omarchy bar plugin move omarchy.clock --section center --index 0 | omarchy bar plugin remove omarchy.tailscale | omarchy bar plugin set omarchy.clock format HH:mm
# omarchy:examples=omarchy bar plugin add omarchy.tailscale | omarchy bar plugin add omarchy.clock center | omarchy bar plugin move omarchy.media left | omarchy bar plugin move omarchy.clock --section center --index 0 | omarchy bar plugin remove omarchy.tailscale | omarchy bar plugin set omarchy.clock format HH:mm
set -euo pipefail
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
OMARCHY_ROOT="${OMARCHY_PATH:-}"
DEFAULTS_FILE="$OMARCHY_ROOT/config/omarchy/shell.json"
source omarchy-shell-config
usage() {
cat <<USAGE
@@ -43,7 +41,8 @@ Widget ids are listed by 'omarchy plugin list'.
Examples:
omarchy bar plugin add omarchy.tailscale
omarchy bar plugin add omarchy.clock --section center --before omarchy.weather
omarchy bar plugin add omarchy.clock center
omarchy bar plugin move omarchy.media left
omarchy bar plugin move omarchy.clock --section center --index 0
omarchy bar plugin remove omarchy.tailscale
omarchy bar plugin remove omarchy.tailscale --all
@@ -52,31 +51,9 @@ Examples:
USAGE
}
fail() {
echo "omarchy-bar-plugin: $*" >&2
exit 1
}
refresh_shell_config() {
if ! omarchy-shell shell reloadConfig >/dev/null 2>&1; then
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
fi
}
source_file() {
if [[ -s $CONFIG_FILE ]]; then
printf '%s\n' "$CONFIG_FILE"
else
printf '%s\n' "$DEFAULTS_FILE"
fi
}
# jq defs shared across mutations. All `def`s come first so the pipeline that
# follows them stays valid jq. `entry_id` and the shape helpers are used by both
# the normalize pipeline and the resolve_target/resolve_source helpers.
# jq defs used by the mutation pipelines. All `def`s come first so the pipeline
# that follows them stays valid jq; NORMALIZE adds its own shape helpers.
JQ_DEFS='
def object_or_empty: if type == "object" then . else {} end;
def array_or_empty: if type == "array" then . else [] end;
def entry_id: if type == "object" then (.id // "" | tostring) else tostring end;
def anchor_for($section): { left: "omarchy.workspaces", center: "omarchy.weather", right: "omarchy.tray" }[$section];
def find_all($id; $only):
@@ -130,40 +107,6 @@ JQ_DEFS='
end;
'
# jq pipeline that normalizes shell.json into a well-shaped object with
# version=1, bar.layout.{left,center,right} arrays, and plugins array. Every
# mutation pipes through this so downstream jq can assume structure.
NORMALIZE='
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
| .bar.layout = (.bar.layout | object_or_empty)
| .bar.layout.left = (.bar.layout.left | array_or_empty)
| .bar.layout.center = (.bar.layout.center | array_or_empty)
| .bar.layout.right = (.bar.layout.right | array_or_empty)
| .plugins = (.plugins | array_or_empty)
'
# Apply a jq program to the source file and atomically write the result to the
# user config, then refresh the running shell. Extra args after the program are
# forwarded to jq (e.g. --arg/--argjson).
_BAR_TMP=""
cleanup_bar_tmp() {
if [[ -n ${_BAR_TMP:-} ]]; then rm -f "$_BAR_TMP"; fi
}
trap cleanup_bar_tmp EXIT
commit() {
local program="$1"
shift
mkdir -p "$(dirname "$CONFIG_FILE")"
_BAR_TMP=$(mktemp)
jq -S -e "$@" "$program" "$(source_file)" >"$_BAR_TMP" || fail "could not update shell config"
mv "$_BAR_TMP" "$CONFIG_FILE"
_BAR_TMP=""
refresh_shell_config
}
validate_section() {
[[ $1 =~ ^(left|center|right)$ ]] || fail "section must be left, center, or right"
}
@@ -323,7 +266,26 @@ cmd_move() {
local id="${1:-}"
[[ -n $id ]] || fail "move requires a widget id"
shift
local positional_section=""
while (( $# > 0 )); do
if [[ $1 == --* ]]; then
break
fi
[[ -z $positional_section ]] || fail "unexpected argument: $1"
positional_section="$1"
shift
done
parse_placement "$@"
[[ -z $positional_section || -z $PLACEMENT_SECTION ]] || fail "specify a section positionally or with --section, not both"
[[ -z $positional_section || -z $PLACEMENT_INDEX ]] || fail "specify a section positionally or use --index, not both"
[[ -z $positional_section || -z $PLACEMENT_BEFORE ]] || fail "specify a section positionally or use --before, not both"
[[ -z $positional_section || -z $PLACEMENT_AFTER ]] || fail "specify a section positionally or use --after, not both"
if [[ -n $positional_section ]]; then
validate_section "$positional_section"
PLACEMENT_SECTION="$positional_section"
fi
local default_section="${PLACEMENT_SECTION:-}"
local prog
@@ -420,7 +382,7 @@ cmd_set() {
parse_placement "$@"
if [[ $value_is_json == "true" ]]; then
jq -e . <<<"$value" >/dev/null 2>&1 || fail "invalid JSON value: $value"
jq -n --argjson value "$value" empty >/dev/null 2>&1 || fail "invalid JSON value: $value"
fi
local value_arg
@@ -474,7 +436,7 @@ cmd_replace() {
local new="${2:-}"
[[ -n $old ]] || fail "replace requires the source widget id"
[[ -n $new ]] || fail "replace requires the replacement widget id"
[[ $# -eq 2 ]] || fail "replace takes two widget ids"
(( $# == 2 )) || fail "replace takes two widget ids"
local prog
prog=$(cat <<JQ
@@ -497,7 +459,7 @@ JQ
# --------------------------------------------------------------------- dispatch
command="${1:-}"
[[ $# -gt 0 ]] && shift || true
(( $# > 0 )) && shift || true
case "$command" in
add)
+18 -2
View File
@@ -35,6 +35,7 @@ RESOLUTION=""
FULLSCREEN="false"
STOP_RECORDING="false"
RECORDING_FILE="/tmp/omarchy-screenrecord-filename"
REGION_FILE="${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region"
LOG_FILE=$([[ ${OMARCHY_SCREENRECORD_DEBUG:-false} == "true" ]] && echo "/tmp/omarchy-screenrecord.log" || echo "/dev/null")
for arg in "$@"; do
@@ -89,11 +90,26 @@ start_webcam_overlay() {
--title="WebcamOverlay" --wayland-app-id="WebcamOverlay-$WEBCAM_SIZE" \
--no-border --no-audio --no-osc --osd-level=0 \
--really-quiet &>/dev/null &
sleep 1
# The move has to settle before gpu-screen-recorder starts, or the camera is
# recorded sliding into its corner. Waiting for the map is what the blind
# second was partly guessing at, so the remainder is trimmed to hold the
# pre-capture delay where it was: starting later costs the first words spoken.
local waited=0
while ((waited < 40)) && ! hyprctl clients -j | jq -e 'any(.[]; .title == "WebcamOverlay")' >/dev/null 2>&1; do
sleep 0.05
((waited++))
done
[[ ${1:-} == region:* ]] && echo "${1#region:}" >"$REGION_FILE"
omarchy-capture-webcam-resize "$WEBCAM_SIZE"
sleep 0.6
}
cleanup_webcam() {
pkill -f "WebcamOverlay" 2>/dev/null
rm -f "$REGION_FILE"
}
default_resolution() {
@@ -155,7 +171,7 @@ start_screenrecording() {
esac
fi
[[ $WEBCAM == "true" ]] && start_webcam_overlay
[[ $WEBCAM == "true" ]] && start_webcam_overlay "$target"
local filename="$OUTPUT_DIR/screenrecording-$(date +'%Y-%m-%d_%H-%M-%S').mp4"
local audio_devices=""
+35 -8
View File
@@ -8,6 +8,7 @@
set -euo pipefail
readonly MARGIN=40
readonly REGION_FILE="${XDG_RUNTIME_DIR:-/tmp}/omarchy-screenrecord-region"
usage() {
echo "Usage: omarchy-capture-webcam-resize <smaller|larger|reset|small|medium|large>" >&2
@@ -57,13 +58,39 @@ read -r monitor_x monitor_y monitor_width monitor_height < <(
[[ $monitor_x =~ ^-?[0-9]+$ && $monitor_y =~ ^-?[0-9]+$ && $monitor_width =~ ^[0-9]+$ && $monitor_height =~ ^[0-9]+$ ]] || exit 0
# Scale the 8:9 portrait presets from monitor height so they occupy the same
# Anchor to the recorded region when there is one, so a window picked on a wide
# display keeps the camera in its own corner. Full-monitor captures, the portal
# backend, and resizes outside a recording publish none and fall back here.
anchor_x=$monitor_x
anchor_y=$monitor_y
anchor_width=$monitor_width
anchor_height=$monitor_height
if [[ -f $REGION_FILE ]] && region=$(<"$REGION_FILE"); then
if [[ $region =~ ^([0-9]+)x([0-9]+)\+(-?[0-9]+)\+(-?[0-9]+)$ ]]; then
anchor_width=${BASH_REMATCH[1]}
anchor_height=${BASH_REMATCH[2]}
anchor_x=${BASH_REMATCH[3]}
anchor_y=${BASH_REMATCH[4]}
fi
fi
# A tall, narrow region can't fit presets scaled from its own height, so cap the
# height they scale from to what the width allows — the large preset is the
# widest at 3/10 of it. Scaling the ladder as a whole leaves small, medium and
# large distinct sizes for smaller and larger to step between.
scale_height=$anchor_height
available_width=$((anchor_width - 2 * MARGIN))
((available_width > 0 && scale_height * 3 / 10 > available_width)) &&
scale_height=$((available_width * 10 / 3))
# Scale the 8:9 portrait presets from that height so they occupy the same
# proportion of a 1080p, HiDPI, ultrawide, or 6K recording.
small_height=$(((monitor_height * 9 + 25) / 50))
small_height=$(((scale_height * 9 + 25) / 50))
small_width=$(((small_height * 8 + 4) / 9))
medium_height=$(((monitor_height + 2) / 4))
medium_height=$(((scale_height + 2) / 4))
medium_width=$(((medium_height * 8 + 4) / 9))
large_height=$(((monitor_height * 27 + 40) / 80))
large_height=$(((scale_height * 27 + 40) / 80))
large_width=$(((large_height * 8 + 4) / 9))
target_width=$current_width
@@ -107,11 +134,11 @@ larger)
;;
esac
target_x=$((monitor_x + monitor_width - target_width - MARGIN))
target_y=$((monitor_y + monitor_height - target_height - MARGIN))
target_x=$((anchor_x + anchor_width - target_width - MARGIN))
target_y=$((anchor_y + anchor_height - target_height - MARGIN))
((target_x < monitor_x + MARGIN)) && target_x=$((monitor_x + MARGIN))
((target_y < monitor_y + MARGIN)) && target_y=$((monitor_y + MARGIN))
((target_x < anchor_x + MARGIN)) && target_x=$((anchor_x + MARGIN))
((target_y < anchor_y + MARGIN)) && target_y=$((anchor_y + MARGIN))
window="address:$address"
hypr_dispatch \
+36 -50
View File
@@ -12,64 +12,38 @@ fail() { echo "Error: $*" >&2; exit 1; }
confirm_dev() {
cat <<'WARNING'
Dev links Omarchy directly to a mutable source checkout.
This disables package-based protections for the Omarchy code path, including
normal package updates and package-backed snapshot restores. You'll be
responsible for pulling, fixing, and restoring that checkout yourself.
The dev channel links Omarchy directly to a checkout of the source in ~/omarchy.
It's exclusively intended for developers working on Omarchy itself.
WARNING
gum confirm --default=false "Enable Dev anyway?"
gum confirm --default=false "Switch to dev channel?"
}
choose_dev_checkout() {
local default_path="$HOME/Work/omarchy"
local path="${OMARCHY_DEV_PATH:-}"
if [[ -z $path ]]; then
path=$(gum input --value "$default_path" --placeholder "$default_path" --header "Where should Dev checkout live? Existing non-checkout paths will not be overwritten.") || exit 1
fi
path="${path:-$default_path}"
case "$path" in
"~") path="$HOME" ;;
"~/"*) path="$HOME/${path#~/}" ;;
/*) ;;
*) path="$PWD/$path" ;;
esac
if [[ -e $path && ! -d $path/.git ]]; then
fail "$path already exists and is not a git checkout. Choose an empty path or an existing Omarchy checkout."
fi
if [[ -d $path/.git && ( ! -d $path/bin || ! -d $path/default || ! -d $path/shell ) ]]; then
fail "$path is a git checkout, but it does not look like Omarchy."
fi
printf '%s\n' "$path"
}
sync_dev_checkout() {
validate_dev_checkout() {
local checkout="$1"
if [[ -d $checkout/.git ]]; then
echo "Updating Dev checkout at $checkout"
git -C "$checkout" fetch origin quattro
git -C "$checkout" checkout quattro 2>/dev/null || git -C "$checkout" checkout --track origin/quattro
git -C "$checkout" pull --ff-only origin quattro
else
mkdir -p "$(dirname -- "$checkout")"
git clone --branch quattro --single-branch https://github.com/basecamp/omarchy.git "$checkout"
if [[ -e $checkout && ! -d $checkout/.git ]]; then
fail "$checkout already exists and is not a git checkout."
fi
omarchy-dev-link "$checkout"
if [[ -d $checkout/.git && ( ! -d $checkout/bin || ! -d $checkout/default || ! -d $checkout/shell ) ]]; then
fail "$checkout is a git checkout, but it does not look like Omarchy."
fi
}
link_dev_checkout() {
local checkout="$1"
[[ -d $checkout/.git ]] || git clone https://github.com/basecamp/omarchy.git "$checkout"
omarchy-dev-link "$checkout" --no-reboot
}
(( $# > 0 )) || { usage; exit 1; }
dev_checkout=""
channel="$1"
leaving_dev=0
case "$channel" in
stable)
@@ -86,7 +60,8 @@ case "$channel" in
;;
dev)
confirm_dev || { echo "Cancelled."; exit 0; }
dev_checkout=$(choose_dev_checkout)
dev_checkout="$HOME/omarchy"
validate_dev_checkout "$dev_checkout"
pacman_channel=edge
packages=(omarchy-dev omarchy-settings-dev)
;;
@@ -97,17 +72,28 @@ case "$channel" in
;;
esac
if [[ -z $dev_checkout && $OMARCHY_PATH != "/usr/share/omarchy" ]]; then
leaving_dev=1
fi
if [[ -n $dev_checkout ]]; then
link_dev_checkout "$dev_checkout"
export OMARCHY_PATH="$dev_checkout"
export PATH="$OMARCHY_PATH/bin:$PATH"
omarchy-state set reboot-required
fi
omarchy-refresh-pacman "$pacman_channel"
# --ask 4 accepts omarchy <-> omarchy-dev replacement prompts without file overwrites.
sudo env OMARCHY_UPDATE_PACMAN=1 pacman -S --needed --noconfirm --ask 4 "${packages[@]}"
if [[ -z $dev_checkout ]] && omarchy-cmd-present omarchy-dev-unlink; then
omarchy-dev-unlink
if [[ -z $dev_checkout ]]; then
omarchy-dev-unlink --no-reboot
export OMARCHY_PATH=/usr/share/omarchy
if (( leaving_dev )); then
omarchy-state set reboot-required
fi
fi
omarchy-update -y
if [[ -n $dev_checkout ]]; then
sync_dev_checkout "$dev_checkout"
fi
+1 -1
View File
@@ -8,7 +8,7 @@
history_index=""
history_path="$HOME/.local/state/omarchy/clipboard-history.json"
while [[ $# -gt 0 ]]; do
while (( $# > 0 )); do
case "$1" in
--history-index)
history_index="${2:-}"
+1 -1
View File
@@ -11,7 +11,7 @@ copy_only=false
history_index=""
text=""
while [[ $# -gt 0 ]]; do
while (( $# > 0 )); do
case "$1" in
--shift-insert)
use_shift_insert=true
+3 -6
View File
@@ -5,7 +5,7 @@
# omarchy:examples=omarchy default browser firefox | omarchy default browser brave
if (($# == 0)); then
case "$(xdg-settings get default-web-browser)" in
case "$(env -u BROWSER xdg-settings get default-web-browser)" in
chromium.desktop) echo "chromium" ;;
google-chrome.desktop) echo "chrome" ;;
brave-browser.desktop) echo "brave" ;;
@@ -13,7 +13,7 @@ if (($# == 0)); then
microsoft-edge.desktop) echo "edge" ;;
firefox.desktop) echo "firefox" ;;
zen.desktop) echo "zen" ;;
*) xdg-settings get default-web-browser ;;
*) env -u BROWSER xdg-settings get default-web-browser ;;
esac
exit 0
fi
@@ -32,9 +32,6 @@ zen) desktop_id="zen.desktop"; name="Zen"; glyph=󰖟 ;;
;;
esac
xdg-settings set default-web-browser "$desktop_id"
xdg-mime default "$desktop_id" x-scheme-handler/http
xdg-mime default "$desktop_id" x-scheme-handler/https
xdg-mime default "$desktop_id" text/html
env -u BROWSER xdg-settings set default-web-browser "$desktop_id" || exit 1
omarchy-notification-send -g $glyph "$name is now the default browser"
+18 -5
View File
@@ -2,8 +2,8 @@
# omarchy:summary=Point Omarchy at a local checkout after reboot
# omarchy:group=dev
# omarchy:args=<path-to-checkout>
# omarchy:examples=omarchy dev link ~/Work/omarchy/omarchy-installer
# omarchy:args=<path-to-checkout> [--no-reboot]
# omarchy:examples=omarchy dev link ~/omarchy
set -euo pipefail
@@ -12,9 +12,11 @@ if (( EUID == 0 )); then
exit 1
fi
if (( $# != 1 )) || [[ $1 == "-h" || $1 == "--help" ]]; then
prompt_reboot=1
if (( $# < 1 || $# > 2 )) || [[ $1 == "-h" || $1 == "--help" ]]; then
cat <<USAGE
Usage: omarchy dev link <path-to-checkout>
Usage: omarchy dev link <path-to-checkout> [--no-reboot]
Writes /etc/omarchy.conf so OMARCHY_PATH resolves to <path-to-checkout>
after reboot. This intentionally does not rewrite the running Hyprland,
@@ -25,10 +27,21 @@ themes/, applications/, config/. Files installed at fixed system paths
(/etc/, /usr/lib/systemd/, udev rule bodies, /etc/skel after user
creation, /usr/share/plymouth) are NOT covered — for those, use
omarchy-dev-pkg-test to build and install the package from the checkout.
Use --no-reboot when another command will handle the reboot prompt.
USAGE
exit 0
fi
if (( $# == 2 )); then
if [[ $2 == "--no-reboot" ]]; then
prompt_reboot=0
else
echo "Usage: omarchy dev link <path-to-checkout> [--no-reboot]" >&2
exit 1
fi
fi
omarchy_conf_quote() {
local value="$1"
value=${value//\\/\\\\}
@@ -58,6 +71,6 @@ done
echo "Pointed Omarchy at $target"
echo
if gum confirm "Reboot now to activate?"; then
if (( prompt_reboot )) && gum confirm "Reboot now to activate?"; then
omarchy-system-reboot
fi
+27 -6
View File
@@ -2,6 +2,7 @@
# omarchy:summary=Restore Omarchy to the package install after reboot
# omarchy:group=dev
# omarchy:args=[--no-reboot]
set -euo pipefail
@@ -10,16 +11,36 @@ if (( EUID == 0 )); then
exit 1
fi
if [[ ${1:-} == "-h" || ${1:-} == "--help" ]]; then
cat <<USAGE
Usage: omarchy dev unlink
prompt_reboot=1
if (( $# > 1 )); then
echo "Usage: omarchy dev unlink [--no-reboot]" >&2
exit 1
fi
case "${1:-}" in
"")
;;
--no-reboot)
prompt_reboot=0
;;
-h|--help)
cat <<USAGE
Usage: omarchy dev unlink [--no-reboot]
Writes /etc/omarchy.conf so OMARCHY_PATH resolves to /usr/share/omarchy
after reboot. This intentionally does not rewrite the running Hyprland,
systemd, shell, or app-launcher environment; reboot to make every layer agree.
Use --no-reboot when another command will handle the reboot prompt.
USAGE
exit 0
fi
exit 0
;;
*)
echo "Usage: omarchy dev unlink [--no-reboot]" >&2
exit 1
;;
esac
default_target="/usr/share/omarchy"
@@ -28,6 +49,6 @@ printf 'export OMARCHY_PATH="%s"\n' "$default_target" | sudo tee /etc/omarchy.co
echo "Pointed Omarchy at $default_target"
echo
if gum confirm "Reboot now to activate?"; then
if (( prompt_reboot )) && gum confirm "Reboot now to activate?"; then
omarchy-system-reboot
fi
+1 -1
View File
@@ -102,7 +102,7 @@ done
source "$OMARCHY_INSTALL/user/all.sh"
omarchy-refresh-applications
xdg-settings set default-web-browser chromium.desktop
env -u BROWSER xdg-settings set default-web-browser chromium.desktop
xdg-mime default HEY.desktop x-scheme-handler/mailto
if (( first_install )); then
+5 -38
View File
@@ -53,37 +53,6 @@ log_first_run() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$FIRST_RUN_LOG"
}
notification_server_ready() {
if omarchy-cmd-present gdbus; then
gdbus call --session \
--dest org.freedesktop.Notifications \
--object-path /org/freedesktop/Notifications \
--method org.freedesktop.Notifications.GetServerInformation >/dev/null 2>&1
elif omarchy-cmd-present busctl; then
busctl --user call \
org.freedesktop.Notifications \
/org/freedesktop/Notifications \
org.freedesktop.Notifications \
GetServerInformation >/dev/null 2>&1
else
return 0
fi
}
wait_for_notifications() {
omarchy-cmd-present omarchy-shell || return 0
for _ in {1..100}; do
if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then
return 0
fi
sleep 0.1
done
log_first_run "Timed out waiting for notification service; continuing"
return 0
}
run_first_run_step() {
local name="$1"
shift
@@ -98,14 +67,10 @@ run_first_run_step() {
fi
}
wait_for_notifications
run_first_run_step "enable migration notification watcher" \
systemctl --user enable --now omarchy-update-user-notify.path
run_first_run_step "notify about pending migrations" omarchy-migrate-notify
run_first_run_step "install Voxtype post-update 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" \
omarchy-hook-install post-update "$OMARCHY_PATH/install/user/first-run/setup-fingerprint.hook"
run_first_run_step "enable user systemd units" \
bash "$OMARCHY_PATH/install/user/first-run/enable-user-units.sh"
@@ -113,8 +78,10 @@ run_first_run_step "set GNOME theme" \
bash "$OMARCHY_PATH/install/user/first-run/gnome-theme.sh"
run_first_run_step "set GTK primary paste" \
bash "$OMARCHY_PATH/install/user/first-run/gtk-primary-paste.sh"
run_first_run_step "apply speaker tuning" \
bash "$OMARCHY_PATH/install/user/first-run/audio-tuning.sh"
wait_for_notifications
omarchy-notification-wait || log_first_run "Timed out waiting for notification service; continuing"
run_first_run_step "show welcome notification" \
bash "$OMARCHY_PATH/install/user/first-run/welcome.sh"
# The first-run notification scripts register action callbacks in background
+18 -30
View File
@@ -9,7 +9,6 @@ usage() {
}
font_name="${1:-}"
omarchy_root="${OMARCHY_PATH:-/usr/share/omarchy}"
case "$font_name" in
-h|--help)
@@ -46,36 +45,25 @@ if [[ -f ~/.config/foot/foot.ini ]]; then
fi
# fontconfig is the canonical source of truth — the omarchy shell, Qt apps,
# and anything resolving "monospace" all read from here. The shipped default
# is package-owned; create a user override only when the user changes fonts.
# and anything resolving "monospace" all read from here. This file is loaded
# after the package-owned default, and prepend_first puts the chosen family at
# the head of the list so it wins over the family that default prefers.
fontconfig_file="$HOME/.config/fontconfig/fonts.conf"
if [[ ! -f $fontconfig_file ]]; then
fontconfig_default="$omarchy_root/default/fontconfig/conf.avail/50-omarchy.conf"
if [[ ! -f $fontconfig_default ]]; then
echo "Default fontconfig file not found: $fontconfig_default" >&2
exit 1
fi
mkdir -p "$(dirname "$fontconfig_file")"
cp "$fontconfig_default" "$fontconfig_file"
fi
# We own the markup in 50-omarchy.conf, so the monospace block is predictable:
# the <string>FAMILY</string> immediately after <string>monospace</string> is
# the family we're replacing.
tmp=$(mktemp)
if ! awk -v new_font="$font_name" '
in_mono && /<string>[^<]*<\/string>/ {
sub(/<string>[^<]*<\/string>/, "<string>" new_font "</string>")
in_mono = 0
}
/<string>monospace<\/string>/ { in_mono = 1 }
{ print }
' "$fontconfig_file" >"$tmp"; then
rm -f "$tmp"
echo "Failed to update $fontconfig_file" >&2
exit 1
fi
mv "$tmp" "$fontconfig_file"
mkdir -p "$(dirname "$fontconfig_file")"
cat >"$fontconfig_file" <<XML
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match target="pattern">
<test name="family" qual="any">
<string>monospace</string>
</test>
<edit name="family" mode="prepend_first" binding="strong">
<string>$font_name</string>
</edit>
</match>
</fontconfig>
XML
omarchy-restart-shell
+2 -11
View File
@@ -3,14 +3,5 @@
# omarchy:summary=Returns true when clamshell mode is active
# omarchy:hidden=true
lid_closed=false
for state in /proc/acpi/button/lid/*/state; do
[[ -r $state ]] || continue
if [[ $(< "$state") == *"closed"* ]]; then
lid_closed=true
break
fi
done
[[ $lid_closed == "true" ]] && omarchy-hw-external-monitors
# Clamshell = lid closed while driving one or more external monitors.
omarchy-hw-laptop-closed && omarchy-hw-external-monitors
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# omarchy:summary=Returns true when a fingerprint reader is present
# omarchy:hidden=true
# Detect straight from sysfs so this works before fprintd/usbutils are
# installed (the fingerprint setup pulls those in). USB vendor IDs listed here
# ship fingerprint readers; multi-purpose vendors (e.g. Elan/STMicro, which
# also make USB touchscreens) are left out to avoid nagging laptops with no
# reader — those still match on the product string below when present.
fingerprint_vendors=" 27c6 138a 06cb 08ff 1c7a 147e "
# 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.
# The other things these vendors build — Synaptics webcam bridges (usbio-bridge
# on the Dell XPS 14), touchpads and touchscreens (usbhid), cameras (uvcvideo)
# — all bind one. Only the vendor-ID guess needs this; a device that names
# itself a fingerprint reader is trusted outright.
has_kernel_driver() {
local intf
for intf in "$1"/*:*; do
[[ -e $intf/driver ]] && return 0
done
return 1
}
for dev in /sys/bus/usb/devices/*; do
# The device's own product descriptor usually names it, e.g. "Goodix
# Fingerprint USB Device" — driver-independent and vendor-agnostic.
if [[ -r $dev/product ]]; then
product=$(<"$dev/product")
product=${product,,}
[[ $product == *fingerprint* || $product == *biometric* ]] && exit 0
fi
if [[ -r $dev/idVendor ]]; then
vendor=$(<"$dev/idVendor")
[[ $fingerprint_vendors == *" $vendor "* ]] &&
! has_kernel_driver "$dev" && exit 0
fi
done
exit 1
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
# omarchy:summary=Returns true when the laptop lid is closed
# omarchy:hidden=true
for state in /proc/acpi/button/lid/*/state; do
[[ -r $state ]] || continue
[[ $(< "$state") == *"closed"* ]] && exit 0
done
exit 1
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# omarchy:summary=Check whether a webcam is available
v4l2-ctl --list-devices 2>/dev/null | grep -q '^[[:space:]]*/dev/video'
+27 -3
View File
@@ -8,7 +8,7 @@ CLAMSHELL_FLAG="$TOGGLES_DIR/internal-monitor-clamshell.lua"
MANUAL_DISABLE_FLAG="$TOGGLES_DIR/internal-monitor-disable.lua"
SCALE_STATE="$TOGGLES_DIR/internal-monitor-scale"
INTERNAL=$(hyprctl monitors all -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DSI)-")).name' | head -n 1)
INTERNAL=$(omarchy-hyprland-monitor-laptop)
valid_scale() {
[[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]]
@@ -30,13 +30,24 @@ configured_monitor_scale() {
local monitor_lua="$HOME/.config/hypr/monitors.lua"
[[ -f $monitor_lua ]] || return 0
local scale
scale=$(sed -n 's/^local omarchy_monitor_scale = //p' "$monitor_lua" | head -1)
scale=$(configured_internal_monitor_value scale)
if [[ -z $scale ]]; then
scale=$(sed -n 's/^local omarchy_monitor_scale = //p' "$monitor_lua" | head -1)
fi
if [[ -z $scale ]]; then
scale=$(sed -nE 's/^hl\.monitor\(\{ output = "", mode = "preferred", position = "auto", scale = ([^ ]+) \}\)/\1/p' "$monitor_lua" | head -1)
fi
echo "$scale"
}
configured_internal_monitor_value() {
local key="$1"
local monitor_lua="$HOME/.config/hypr/monitors.lua"
[[ -n $INTERNAL && -f $monitor_lua ]] || return 0
sed -nE '/^hl\.monitor\(\{.*output = "'"$INTERNAL"'".*\}\)/s/.*'"$key"' = "?([^", }]+)"?.*/\1/p' "$monitor_lua" | head -1
}
current_internal_scale() {
[[ -n $INTERNAL ]] || return 0
hyprctl monitors all -j | jq -r --arg internal "$INTERNAL" '.[] | select(.name == $internal and .disabled != true) | .scale' | head -1
@@ -75,11 +86,24 @@ read_monitor_scale() {
echo 2
}
read_monitor_position() {
local position
position=$(configured_internal_monitor_value position)
if [[ $position =~ ^[-[:alnum:]_.+]+$ ]]; then
echo "$position"
return
fi
echo auto
}
enable_internal_output() {
[[ -n $INTERNAL ]] || return 0
local scale="${1:-}"
local position
[[ -n $scale ]] || scale=$(read_monitor_scale)
hyprctl eval "hl.monitor({ output = \"$INTERNAL\", mode = \"preferred\", position = \"auto\", scale = $scale })" >/dev/null 2>&1 || true
position=$(read_monitor_position)
hyprctl eval "hl.monitor({ output = \"$INTERNAL\", mode = \"preferred\", position = \"$position\", scale = $scale })" >/dev/null 2>&1 || true
}
sync_internal_scale() {
+1 -2
View File
@@ -7,8 +7,7 @@ TOGGLE="internal-monitor-disable"
TOGGLE_FLAG="$HOME/.local/state/omarchy/toggles/hypr/$TOGGLE.lua"
MIRROR_TOGGLE="internal-monitor-mirror"
# Get internal monitor name dynamically, including disabled outputs.
INTERNAL=$(hyprctl monitors all -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DSI)-")).name' | head -n 1)
INTERNAL=$(omarchy-hyprland-monitor-laptop)
wake() {
hyprctl dispatch 'hl.dsp.dpms({ action = "enable" })' >/dev/null 2>&1 || true
+2 -3
View File
@@ -7,9 +7,8 @@ TOGGLE="internal-monitor-mirror"
TOGGLE_FLAG="$HOME/.local/state/omarchy/toggles/hypr/$TOGGLE.lua"
DISABLE_TOGGLE="internal-monitor-disable"
# Get names dynamically
INTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DSI)-")).name' | head -n 1)
# Get the first available external monitor
INTERNAL=$(omarchy-hyprland-monitor-laptop)
# The first active external monitor
EXTERNAL=$(hyprctl monitors -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DSI)-") | not).name' | head -n 1)
on() {
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# omarchy:summary=Print the name of the built-in laptop display, including disabled outputs.
hyprctl monitors all -j | jq -r '.[] | select(.name | test("^(eDP|LVDS|DSI)-")).name' | head -n 1
+57 -16
View File
@@ -1,7 +1,7 @@
#!/bin/bash
# omarchy:summary=Show, set, or adjust focused Hyprland monitor scaling
# omarchy:args=[up|down|1|1.25|1.6|2|3|4]
# omarchy:args=[up|down|SCALE]
# omarchy:examples=omarchy hyprland monitor scaling | omarchy hyprland monitor scaling 1.6 | omarchy hyprland monitor scaling up | omarchy hyprland monitor scaling down
SCALES=(1 1.25 1.6 2 3 4)
@@ -9,7 +9,7 @@ STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy"
SCALE_LOG="$STATE_DIR/monitor-scaling.log"
usage() {
echo "Usage: omarchy-hyprland-monitor-scaling [up|down|1|1.25|1.6|2|3|4]"
echo "Usage: omarchy-hyprland-monitor-scaling [up|down|SCALE]"
}
focused_monitor_scale() {
@@ -54,8 +54,7 @@ audit_scale_change() {
# Hyprland only accepts scales where the mode divides into whole logical
# pixels (in 1/120 steps), so clean scales are divisors of gcd(w*120, h*120).
# Round the requested scale up to the nearest one, so 3x on a 1280x800 QEMU
# display becomes 3.2x instead of a red error overlay.
# Round the requested scale up to the nearest clean value.
clean_scale() {
awk -v scale="$1" -v width="$2" -v height="$3" '
function gcd(a, b, t) { while (b) { t = a % b; a = b; b = t } return a }
@@ -68,6 +67,10 @@ clean_scale() {
}'
}
normalize_scale() {
awk 'NR == 1 { printf "%g\n", $0 }'
}
set_scale() {
local requested_scale="$1"
local requested="${2:-$requested_scale}"
@@ -100,21 +103,50 @@ set_scale() {
scale_from_current() {
local direction="${1:-}"
local width="${2:-}"
local height="${3:-}"
awk -v direction="$direction" -v list="${SCALES[*]}" '
awk -v direction="$direction" -v list="${SCALES[*]}" -v width="$width" -v height="$height" '
function gcd(a, b, t) { while (b) { t = a % b; a = b; b = t } return a }
function clean(scale, g, k) {
g = gcd(width * 120, height * 120)
k = int(scale * 120 + 0.5)
if (k > g) k = g
while (g % k != 0) k++
return k / 120
}
NR == 1 { scale = $0; found = 1 }
END {
if (!found) exit 1
n = split(list, scales, " ")
preset_count = split(list, presets, " ")
for (i = 1; i <= preset_count; i++) {
effective = clean(presets[i])
key = sprintf("%.8f", effective)
distance = presets[i] - effective
if (distance < 0) distance = -distance
# Snap to the nearest preset first. Hyprland reports scales as floating
# point values, so a scale set to 3 can come back as 3.0000000000000004.
# Without snapping, the directional comparisons below can misidentify
# the current preset and refuse to step down (or up) any further.
# Multiple presets can collapse to the same clean scale. Keep only the
# closest label so stepping always moves to a distinct effective value.
if (!(key in effective_index)) {
effective_index[key] = ++n
effective_scales[n] = effective
scales[n] = presets[i]
distances[n] = distance
} else {
idx = effective_index[key]
if (distance < distances[idx]) {
scales[idx] = presets[i]
distances[idx] = distance
}
}
}
# Snap to the nearest effective scale first. Hyprland reports floating
# point values, so exact comparisons can otherwise get stuck.
best = 1; best_diff = 1e9
for (i = 1; i <= n; i++) {
diff = scale - scales[i]; if (diff < 0) diff = -diff
diff = scale - effective_scales[i]; if (diff < 0) diff = -diff
if (diff < best_diff) { best_diff = diff; best = i }
}
@@ -130,22 +162,31 @@ scale_from_current() {
case "${1:-}" in
"")
focused_monitor_scale | scale_from_current
focused_monitor_scale | normalize_scale
;;
-h | --help)
usage
;;
up)
set_scale "$(focused_monitor_scale | scale_from_current next)" "up"
monitor_info=$(hyprctl monitors -j | jq -e -c '.[] | select(.focused == true)')
set_scale "$(echo "$monitor_info" | jq -r '.scale' | scale_from_current next \
"$(echo "$monitor_info" | jq -r '.width')" "$(echo "$monitor_info" | jq -r '.height')")" "up"
;;
down)
set_scale "$(focused_monitor_scale | scale_from_current previous)" "down"
monitor_info=$(hyprctl monitors -j | jq -e -c '.[] | select(.focused == true)')
set_scale "$(echo "$monitor_info" | jq -r '.scale' | scale_from_current previous \
"$(echo "$monitor_info" | jq -r '.width')" "$(echo "$monitor_info" | jq -r '.height')")" "down"
;;
1 | 1.25 | 1.6 | 2 | 3 | 4)
set_scale "$1" "$1"
;;
*)
usage >&2
exit 1
if [[ $1 =~ ^[0-9]+([.][0-9]+)?$ ]] &&
awk -v scale="$1" 'BEGIN { exit !(scale >= 1 && scale <= 4) }'; then
set_scale "$1" "$1"
else
usage >&2
exit 1
fi
;;
esac
+7 -1
View File
@@ -3,13 +3,19 @@
# omarchy:summary=Toggle the layout on the current active workspace between dwindle and scrolling
ACTIVE_WORKSPACE=$(hyprctl activeworkspace -j | jq -r '.id')
[[ $ACTIVE_WORKSPACE =~ ^-?[0-9]+$ ]] || exit 1
CURRENT_LAYOUT=$(hyprctl activeworkspace -j | jq -r '.tiledLayout')
LAYOUTS_DIR="$HOME/.local/state/omarchy/workspace-layouts"
LAYOUT_FILE="$LAYOUTS_DIR/$ACTIVE_WORKSPACE.lua"
case "$CURRENT_LAYOUT" in
dwindle) NEW_LAYOUT=scrolling ;;
*) NEW_LAYOUT=dwindle ;;
esac
mkdir -p "$LAYOUTS_DIR"
printf 'hl.workspace_rule({ workspace = "%s", layout = "%s" })\n' "$ACTIVE_WORKSPACE" "$NEW_LAYOUT" >"$LAYOUT_FILE"
hyprctl eval "hl.workspace_rule({ workspace = \"$ACTIVE_WORKSPACE\", layout = \"$NEW_LAYOUT\" })" >/dev/null 2>&1 || \
hyprctl keyword workspace $ACTIVE_WORKSPACE, layout:$NEW_LAYOUT
hyprctl keyword workspace "$ACTIVE_WORKSPACE, layout:$NEW_LAYOUT"
omarchy-notification-send -g 󱂬 "Workspace layout set to $NEW_LAYOUT"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
# omarchy:summary=Install Emacs with Omarchy theme and font integration via the omarchy-emacs AUR package
echo "Installing Emacs..."
omarchy-pkg-aur-add omarchy-emacs && omarchy-install-emacs
# emacsclient opens a frame on the running daemon, not a second Emacs
setsid gtk-launch emacsclient
+1 -1
View File
@@ -3,7 +3,7 @@
# omarchy:summary=Launch the default browser as determined by xdg-settings.
# omarchy:args=[url]
default_browser=$(xdg-settings get default-web-browser)
default_browser=$(env -u BROWSER xdg-settings get default-web-browser)
browser_exec=$(sed -n 's/^Exec=\([^ ]*\).*/\1/p' {~/.local,~/.nix-profile,/usr}/share/applications/$default_browser 2>/dev/null | head -1)
if $browser_exec --help 2>/dev/null | grep -q MOZ_LOG; then
+27 -3
View File
@@ -17,6 +17,14 @@ fi
focused=$(omarchy-hyprland-monitor-focused)
terminal=$(xdg-terminal-exec --print-id)
case $terminal in
*Alacritty* | *ghostty* | *foot* | *kitty*) ;;
*)
omarchy-notification-send -g ✋ "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty"
exit 1
;;
esac
hypr_focus_monitor() {
hyprctl dispatch "hl.dsp.focus({ monitor = \"$1\" })" >/dev/null 2>&1 || hyprctl dispatch focusmonitor "$1" >/dev/null
}
@@ -28,6 +36,23 @@ hypr_exec() {
hyprctl dispatch "hl.dsp.exec_cmd([[$command]])" >/dev/null 2>&1 || hyprctl dispatch exec -- bash -lc "$command" >/dev/null
}
SOCKET="$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock"
# Open Hyprland's event stream before spawning anything, so a terminal that maps
# quickly can't emit its openwindow event before we are listening for it.
exec {events}< <(socat -U - "UNIX-CONNECT:$SOCKET")
# hypr_exec is async and a new window maps on whatever monitor is focused at that
# moment. Block until this monitor's screensaver actually opens before moving
# focus on -- otherwise slow-starting terminals all pile onto the last monitor.
# The deadline is a safety net in case the window never appears.
wait_for_screensaver_window() {
local line deadline=$((SECONDS + 5))
while ((SECONDS < deadline)) && IFS= read -r -t $((deadline - SECONDS)) -u "$events" line; do
[[ $line == openwindow\>\>*,org.omarchy.screensaver,* ]] && return 0
done
}
for m in $(hyprctl monitors -j | jq -r '.[] | .name'); do
hypr_focus_monitor "$m"
@@ -44,10 +69,9 @@ for m in $(hyprctl monitors -j | jq -r '.[] | .name'); do
*kitty*)
hypr_exec kitty --class=org.omarchy.screensaver --override font_size=18 --override window_padding_width=0 -e omarchy-screensaver
;;
*)
omarchy-notification-send -g ✋ "Screensaver only runs in Alacritty, Foot, Ghostty, or Kitty"
;;
esac
wait_for_screensaver_window
done
hypr_focus_monitor "$focused"
+8 -8
View File
@@ -45,13 +45,9 @@ parse_keycodes() {
{
if (match($0, /code:([0-9]+)/, match_parts)) {
code = match_parts[1]
if (code == "201") {
sub(/SUPER SHIFT,code:201/, ",COPILOT KEY")
} else {
symbol = keycode_symbol[code]
if (symbol == "") symbol = "code:" code
sub("code:" code, symbol)
}
symbol = keycode_symbol[code]
if (symbol == "") symbol = "code:" code
sub("code:" code, symbol)
} else if (match($0, /mouse:([0-9]+)/, match_parts)) {
code = match_parts[1]
symbol = mouse_symbol[code]
@@ -304,6 +300,9 @@ dynamic_bindings() {
[[ -z $description && $dispatcher == "__lua" ]] && continue
# The Copilot key just duplicates an existing binding, so keep it hidden
[[ $key == "code:201" ]] && continue
case "$key" in
comma) key="COMMA" ;;
period) key="PERIOD" ;;
@@ -419,6 +418,7 @@ prioritize_entries() {
if (match(line, /Toggle workspace gaps/)) prio = 39
if (match(line, /Toggle nightlight/)) prio = 40
if (match(line, /Toggle locking/)) prio = 41
if (match(line, /Jump to waiting Tmux pane/)) prio = 42
if (match(line, /group/)) prio = 94
if (match(line, /Scroll active workspace/)) prio = 95
if (match(line, /Cycle to/)) prio = 96
@@ -456,7 +456,7 @@ output_binding_records_uncached() {
keybindings_cache_key() {
{
printf 'v6\n'
printf 'v8\n'
hyprctl devices 2>/dev/null | grep -F 'active keymap:'
hyprctl binds 2>/dev/null
} | sha256sum | awk '{ print $1 }'
+3 -2
View File
@@ -96,6 +96,7 @@ while IFS=$'\t' read -r name file marker; do
fi
done < <(migration_entries)
# Clear notifications queued while an update was installing migrations. The
# substring matches both the current and legacy notification titles.
# Clear a login-time notification the user left sitting there and then resolved
# by running migrations some other way. The substring matches both the current
# and legacy notification titles.
omarchy-notification-dismiss "Omarchy Migrations" >/dev/null 2>&1 || true
+10 -4
View File
@@ -15,11 +15,17 @@ fi
notify_command=$(printf 'if [[ -n $(omarchy-notification-send -u critical -g  "Pending Omarchy Migrations" %q -a) ]]; then omarchy-launch-floating-terminal-with-presentation omarchy-migrate; fi' "$message")
if omarchy-cmd-present systemd-run; then
unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)"
systemd-run --user --scope --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0
fi
# This runs from omarchy-migrate-notify.service at graphical-session.target,
# which the session can reach before the shell has claimed
# org.freedesktop.Notifications. Without the wait the toast is sent into the
# void and the user never learns about their pending migrations.
omarchy-notification-wait || true
unit="omarchy-migrations-notification-$(date +%Y%m%d%H%M%S)"
systemd-run --user --scope --unit="$unit" bash -lc "$notify_command" >/dev/null 2>&1 && exit 0
# Reached when there is no user manager to run the scope under, such as a
# non-graphical shell, so fall back to telling the user in the terminal.
print_pending_migrations() {
echo "Omarchy has pending migrations. Run omarchy-migrate in a terminal to apply them:"
while IFS= read -r migration; do
+1 -1
View File
@@ -17,7 +17,7 @@ mkdir -p "$HOME/.local/bin"
cat >"$HOME/.local/bin/$command" <<EOF
#!/bin/bash
mise use -g "$package"
exec "$bin" "\$@"
exec mise exec "$package" -- "$bin" "\$@"
EOF
chmod +x "$HOME/.local/bin/$command"
+2 -1
View File
@@ -18,4 +18,5 @@ printf '%s\n' "$monitors_json" | jq -r '
omarchy-hyprland-monitor-focused 2>/dev/null || echo
omarchy-hyprland-monitor-scaling 2>/dev/null || echo
printf '%s\n' "$monitors_json" | jq -c '[.[] | {name, enabled:(.disabled != true), focused:(.focused == true)}]'
printf '%s\n' "$monitors_json" | jq -c \
'[.[] | {name, enabled:(.disabled != true), focused:(.focused == true), width, height}]'
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# omarchy:summary=Wait for the desktop notification server to accept notifications
# omarchy:args=[timeout-seconds]
# omarchy:hidden=true
set -uo pipefail
timeout=${1:-10}
notification_server_ready() {
busctl --user call \
org.freedesktop.Notifications \
/org/freedesktop/Notifications \
org.freedesktop.Notifications \
GetServerInformation >/dev/null 2>&1
}
# The shell has to be up to serve the IPC, and it has to have claimed the
# notification bus name before notify-send has anywhere to deliver.
attempts=$((timeout * 10))
while (( attempts > 0 )); do
if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then
exit 0
fi
attempts=$((attempts - 1))
sleep 0.1
done
exit 1
+3 -6
View File
@@ -1,8 +1,8 @@
#!/bin/bash
# omarchy:summary=Show the Omarchy Quickshell on-screen display
# omarchy:args=[-i|--icon <icon>] [-m|--message <text>] [-p|--progress <0-100>] [-d|--duration <ms>] [-f|--fit]
# omarchy:examples=omarchy osd -i brightness -p 50 | omarchy osd -m "Hello" | omarchy osd -m "Done" --fit
# omarchy:args=[-i|--icon <icon>] [-m|--message <text>] [-p|--progress <0-100>] [-d|--duration <ms>]
# omarchy:examples=omarchy osd -i brightness -p 50 | omarchy osd -m "Hello"
set -euo pipefail
@@ -12,7 +12,6 @@ progress=""
progress_text=""
max="100"
duration=""
fit=0
while (($#)); do
case $1 in
@@ -20,7 +19,6 @@ while (($#)); do
-m|--message) message="${2:-}"; shift 2 ;;
-p|--progress) progress="${2:-}"; shift 2 ;;
-d|--duration) duration="${2:-}"; shift 2 ;;
-f|--fit) fit=1; shift ;;
-h|--help) omarchy osd --help; exit 0 ;;
*) echo "Unknown OSD option: $1" >&2; exit 1 ;;
esac
@@ -37,7 +35,6 @@ payload=$(jq -cn \
--arg progressText "$progress_text" \
--arg max "$max" \
--arg duration "$duration" \
--argjson fit "$fit" \
'{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max,duration:$duration,fit:$fit}')
'{icon:$icon,message:$message,value:$value,progressText:$progressText,max:$max,duration:$duration}')
omarchy-shell -q osd show "$payload"
+2 -2
View File
@@ -151,7 +151,7 @@ plugin_enabled() {
[[ -n $id ]] || fail "plugin id is required"
shift 2
if [[ $enabled != "true" && $# -gt 0 ]]; then
if [[ $enabled != "true" ]] && (( $# > 0 )); then
fail "disable does not take placement options"
fi
@@ -165,7 +165,7 @@ plugin_enabled() {
result=$(omarchy-shell shell setPluginEnabled "$id" "$enabled") || fail "could not update plugin; is omarchy-shell running?"
[[ $result == "ok" ]] || fail "plugin '$id' is not known; run: omarchy plugin rescan"
if [[ $enabled == "true" && $# -gt 0 ]]; then
if [[ $enabled == "true" ]] && (( $# > 0 )); then
omarchy-bar-plugin move "$id" "$@"
echo "Enabled and moved $id"
elif [[ $enabled == "true" ]]; then
+1 -7
View File
@@ -13,13 +13,7 @@ if cmp -s "$OMARCHY_PATH/default/plymouth/logo.png" "$installed_logo"; then
fi
omarchy-plymouth-list | while read -r name; do
if [[ -d ~/.config/omarchy/themes/$name ]]; then
theme_dir=~/.config/omarchy/themes/$name
else
theme_dir="$OMARCHY_PATH/themes/$name"
fi
if cmp -s "$theme_dir/unlock.png" "$installed_logo"; then
if cmp -s "$(omarchy-theme-dir "$name")/unlock.png" "$installed_logo"; then
echo "$name"
exit
fi
+1 -7
View File
@@ -7,13 +7,7 @@
find ~/.config/omarchy/themes/ -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -printf '%f\n' 2>/dev/null
find "$OMARCHY_PATH/themes/" -mindepth 1 -maxdepth 1 -type d -printf '%f\n'
} | sort -u | while read -r name; do
if [[ -d ~/.config/omarchy/themes/$name ]]; then
theme_dir=~/.config/omarchy/themes/$name
else
theme_dir="$OMARCHY_PATH/themes/$name"
fi
if [[ -f $theme_dir/preview-unlock.png ]]; then
if [[ -f $(omarchy-theme-dir "$name")/preview-unlock.png ]]; then
echo "$name"
fi
done
+1 -11
View File
@@ -3,15 +3,5 @@
# omarchy:summary=Restore the default Omarchy Plymouth boot theme and SDDM login screen
# omarchy:requires-sudo=true
theme_dir="/usr/share/plymouth/themes/omarchy"
sudo find "$OMARCHY_PATH/default/plymouth" -maxdepth 1 -type f -exec cp -t "$theme_dir/" {} +
sudo plymouth-set-default-theme omarchy
if omarchy-cmd-present limine-mkinitcpio; then
sudo limine-mkinitcpio
else
sudo mkinitcpio -P
fi
omarchy-refresh-plymouth
omarchy-refresh-sddm
+1 -5
View File
@@ -14,11 +14,7 @@ fi
theme=$1
if [[ -d ~/.config/omarchy/themes/$theme ]]; then
theme_dir=~/.config/omarchy/themes/$theme
else
theme_dir="$OMARCHY_PATH/themes/$theme"
fi
theme_dir=$(omarchy-theme-dir "$theme")
theme_color() {
local key="$1"
+1 -7
View File
@@ -10,13 +10,7 @@ mkdir -p "$preview_dir"
ln -s "$OMARCHY_PATH/default/plymouth/preview-unlock.png" "$preview_dir/default.png"
omarchy-plymouth-list | while read -r name; do
if [[ -d ~/.config/omarchy/themes/$name ]]; then
theme_dir=~/.config/omarchy/themes/$name
else
theme_dir="$OMARCHY_PATH/themes/$name"
fi
ln -s "$theme_dir/preview-unlock.png" "$preview_dir/$name.png" 2>/dev/null
ln -s "$(omarchy-theme-dir "$name")/preview-unlock.png" "$preview_dir/$name.png" 2>/dev/null
done
current=$(omarchy-plymouth-current)
+2 -5
View File
@@ -7,17 +7,14 @@
set_fallback_default_browser() {
local current_browser
current_browser=$(xdg-settings get default-web-browser)
current_browser=$(env -u BROWSER xdg-settings get default-web-browser)
if [[ $current_browser != $1 ]]; then
return
fi
if omarchy-cmd-present chromium; then
xdg-settings set default-web-browser chromium.desktop
xdg-mime default chromium.desktop x-scheme-handler/http
xdg-mime default chromium.desktop x-scheme-handler/https
xdg-mime default chromium.desktop text/html
env -u BROWSER xdg-settings set default-web-browser chromium.desktop || true
fi
}
+7 -7
View File
@@ -7,16 +7,16 @@ set -e
remove_pam_config() {
# Remove from sudo
if grep -q pam_fprintd.so /etc/pam.d/sudo; then
# Remove from sudo (both the fingerprint module and its clamshell gate)
if grep -Eq 'pam_fprintd\.so|omarchy-hw-laptop-closed' /etc/pam.d/sudo; then
echo "Removing fingerprint authentication from sudo..."
sudo sed -i '/pam_fprintd\.so/d' /etc/pam.d/sudo
sudo sed -i -e '/pam_fprintd\.so/d' -e '/omarchy-hw-laptop-closed/d' /etc/pam.d/sudo
fi
# Remove from polkit
if [[ -f /etc/pam.d/polkit-1 ]] && grep -Fq 'pam_fprintd.so' /etc/pam.d/polkit-1; then
# Remove from polkit (both the fingerprint module and its clamshell gate)
if [[ -f /etc/pam.d/polkit-1 ]] && grep -Eq 'pam_fprintd\.so|omarchy-hw-laptop-closed' /etc/pam.d/polkit-1; then
echo "Removing fingerprint authentication from polkit..."
sudo sed -i '/pam_fprintd\.so/d' /etc/pam.d/polkit-1
sudo sed -i -e '/pam_fprintd\.so/d' -e '/omarchy-hw-laptop-closed/d' /etc/pam.d/polkit-1
fi
}
@@ -34,6 +34,6 @@ remove_pam_config
remove_lock_fingerprint_pam
echo "Removing fingerprint packages..."
omarchy-pkg-drop fprintd libfprint-git
omarchy-pkg-drop fprintd libfprint libfprint-git
echo -e "\e[32mFingerprint authentication has been completely removed.\e[0m"
+16 -2
View File
@@ -3,7 +3,12 @@
# omarchy:summary=Restart the Omarchy shell
# omarchy:examples=omarchy restart shell
CONFIG_DIR="$OMARCHY_PATH/shell"
# A caller opened after dev link/unlink may disagree with the still-running
# desktop. The user manager receives Hyprland's environment at session start.
session_omarchy_path=$(systemctl --user show-environment 2>/dev/null | sed -n 's/^OMARCHY_PATH=//p' | tail -n 1)
: "${session_omarchy_path:=$OMARCHY_PATH}"
CONFIG_DIR="$session_omarchy_path/shell"
[[ -f $CONFIG_DIR/shell.qml ]] || { echo "Omarchy shell config not found: $CONFIG_DIR" >&2; exit 1; }
# Allow running from outside the session (e.g. over ssh) by deriving the
@@ -28,7 +33,16 @@ while timeout 5 quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1;
hyprctl dispatch 'hl.dsp.exec_cmd("quickshell -n -p $OMARCHY_PATH/shell")' >/dev/null
for (( attempt = 0; attempt < 20; attempt++ )); do
OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1 && exit 0
if OMARCHY_PATH="$session_omarchy_path" OMARCHY_SHELL_IPC_TIMEOUT=0.5s omarchy-shell shell ping >/dev/null 2>&1; then
# Invitation toasts (like Voxtype/fingerprint setup) die with the old
# shell, and their notify-send waiters hang forever: the dying server
# never emits NotificationClosed. A still-running omarchy-*-invitation
# unit is therefore an unanswered invitation — re-run it so its toast
# reappears on the new shell. Answered invitations have already exited
# and been collected, so the glob no longer matches them.
systemctl --user try-restart 'omarchy-*-invitation.service' 2>/dev/null || true
exit 0
fi
sleep 0.1
done
+1 -1
View File
@@ -2,6 +2,6 @@
# omarchy:summary=Restart tmux if running with the latest configuration
if pgrep -x tmux; then
if tmux has-session 2>/dev/null; then
tmux source-file ~/.config/tmux/tmux.conf
fi
@@ -13,15 +13,18 @@ if ! efibootmgr &>/dev/null; then
exit 1
fi
if cat /sys/class/dmi/id/bios_vendor 2>/dev/null | grep -qi "American Megatrends"; then
echo "Error: American Megatrends firmware may not safely support custom EFI entries" >&2
exit 1
fi
bios_vendor=$(cat /sys/class/dmi/id/bios_vendor 2>/dev/null)
if cat /sys/class/dmi/id/bios_vendor 2>/dev/null | grep -qi "Apple"; then
echo "Error: Apple firmware uses its own boot manager" >&2
exit 1
fi
case "${bios_vendor,,}" in
*"american megatrends"*)
echo "Error: American Megatrends firmware may not safely support custom EFI entries" >&2
exit 1
;;
*apple*)
echo "Error: Apple firmware uses its own boot manager" >&2
exit 1
;;
esac
existing_entry=$(efibootmgr | grep -E "^Boot[0-9A-Fa-f]+\*? Omarchy([[:space:]]|$)" | head -1)
+2 -15
View File
@@ -12,8 +12,6 @@ if [[ -z $target_user && -n ${PKEXEC_UID:-} ]]; then
target_user=$(getent passwd "$PKEXEC_UID" | cut -d: -f1)
fi
target_user=${target_user:-$USER}
target_home=$(getent passwd "$target_user" | cut -d: -f6)
target_group=$(id -gn "$target_user" 2>/dev/null || echo "$target_user")
as_root() {
if (( EUID == 0 )); then
@@ -23,19 +21,9 @@ as_root() {
fi
}
tee_root() {
local path="$1"
if (( EUID == 0 )); then
tee "$path"
else
sudo tee "$path"
fi
}
echo "Configuring lock screen password authentication..."
tee_root /etc/pam.d/omarchy-lock-password >/dev/null <<'EOF'
as_root tee /etc/pam.d/omarchy-lock-password >/dev/null <<'EOF'
#%PAM-1.0
auth required pam_faillock.so preauth silent deny=10 unlock_time=120
-auth [success=2 default=ignore] pam_systemd_home.so
@@ -49,7 +37,7 @@ EOF
if omarchy-cmd-present fprintd-list && fprintd-list "$target_user" 2>/dev/null | grep -qi finger; then
echo "Configuring lock screen fingerprint authentication..."
tee_root /etc/pam.d/omarchy-lock-fingerprint >/dev/null <<'EOF'
as_root tee /etc/pam.d/omarchy-lock-fingerprint >/dev/null <<'EOF'
#%PAM-1.0
auth required pam_fprintd.so
account include system-local-login
@@ -58,7 +46,6 @@ else
as_root rm -f /etc/pam.d/omarchy-lock-fingerprint
fi
# omarchy-shell can't reach a running shell during chroot install. The echo
# is just confirmation, so swallow the failure rather than letting it become
# the script's exit code.
+40 -29
View File
@@ -6,32 +6,43 @@
set -e
check_fingerprint_hardware() {
# Get fingerprint devices for the user
devices=$(fprintd-list "$USER" 2>/dev/null)
# Exit if no devices found
if [[ -z $devices ]]; then
echo -e "\e[31m\nNo fingerprint sensor detected.\e[0m"
return 1
fi
return 0
}
setup_pam_config() {
# A clamshell gate runs before pam_fprintd in every stack: when the lid is
# shut the reader is unreachable, so it skips fingerprint (success=1) and PAM
# drops straight to the password prompt instead of blocking on the reader
# until it times out. Lid open → fingerprint, then password as the fallback.
#
# pam_exec needs a literal absolute path (no env expansion). Point at the
# fixed /usr/bin path the omarchy package always provides, so the gate keeps
# working across package installs and dev-link — the latter overlays
# $OMARCHY_PATH trees but leaves /usr/bin untouched.
local fprintd_gate="auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed"
# Configure sudo
if ! grep -q pam_fprintd.so /etc/pam.d/sudo; then
echo "Configuring sudo for fingerprint authentication..."
sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/sudo
sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/sudo
fi
if ! grep -q 'omarchy-hw-laptop-closed' /etc/pam.d/sudo; then
echo "Adding clamshell gate to sudo..."
# Insert immediately before pam_fprintd so success=1 skips exactly it.
sudo sed -i "/pam_fprintd\.so/i $fprintd_gate" /etc/pam.d/sudo
fi
# Configure polkit
if [[ -f /etc/pam.d/polkit-1 ]] && ! grep -q 'pam_fprintd.so' /etc/pam.d/polkit-1; then
echo "Configuring polkit for fingerprint authentication..."
sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/polkit-1
elif [[ ! -f /etc/pam.d/polkit-1 ]]; then
if [[ -f /etc/pam.d/polkit-1 ]]; then
if ! grep -q 'pam_fprintd.so' /etc/pam.d/polkit-1; then
echo "Configuring polkit for fingerprint authentication..."
sudo sed -i '1i auth sufficient pam_fprintd.so' /etc/pam.d/polkit-1
fi
if ! grep -q 'omarchy-hw-laptop-closed' /etc/pam.d/polkit-1; then
echo "Adding clamshell gate to polkit..."
sudo sed -i "/pam_fprintd\.so/i $fprintd_gate" /etc/pam.d/polkit-1
fi
else
echo "Creating polkit configuration with fingerprint authentication..."
sudo tee /etc/pam.d/polkit-1 >/dev/null <<'EOF'
sudo tee /etc/pam.d/polkit-1 >/dev/null <<EOF
$fprintd_gate
auth sufficient pam_fprintd.so
auth required pam_unix.so
@@ -54,23 +65,23 @@ EOF
echo -e "\e[32mSetting up fingerprint scanner for authentication.\n\e[0m"
# Bail before installing anything if there's no reader to talk to.
if ! omarchy-hw-fingerprint; then
echo -e "\e[31mNo fingerprint sensor detected.\e[0m"
exit 1
fi
# Install required packages
echo "Installing required packages..."
installed_libfprint=$(pacman -Qq libfprint 2>/dev/null || true)
# libfprint-git provides+conflicts libfprint; pacman -S --noconfirm
# defaults the conflict prompt to N and aborts. Pre-remove the exact
# libfprint package, but not an installed provider like libfprint-git.
if [[ $installed_libfprint == "libfprint" ]]; then
sudo pacman -Rdd --noconfirm libfprint
# defaults the conflict prompt to N and aborts. Pre-remove it (deps-only,
# so an installed fprintd stays put) so stock libfprint installs cleanly.
if pacman -Q libfprint-git &>/dev/null; then
sudo pacman -Rdd --noconfirm libfprint-git
fi
omarchy-pkg-add libfprint-git fprintd usbutils
if ! check_fingerprint_hardware; then
exit 1
fi
omarchy-pkg-add libfprint fprintd usbutils
# Configure PAM
setup_pam_config
+1 -1
View File
@@ -89,7 +89,7 @@ authorize_keys_from_github() {
authorize_key "$key" && added=$((added + 1))
done <<<"$keys"
if [[ $added -eq 0 ]]; then
if (( added == 0 )); then
echo -e "\e[31mNo valid SSH keys found for GitHub user '$username'.\e[0m" >&2
exit 1
fi
+3 -13
View File
@@ -73,21 +73,11 @@ export PATH="$OMARCHY_PATH/bin:$PATH"
source "$OMARCHY_INSTALL/helpers/logging.sh"
start_install_log
run_logged "$OMARCHY_INSTALL/config/theme-system.sh"
run_logged "$OMARCHY_INSTALL/config/increase-lockout-limit.sh"
run_logged "$OMARCHY_INSTALL/config/lockscreen-pam.sh"
run_logged "$OMARCHY_INSTALL/config/fix-powerprofilesctl-shebang.sh"
run_logged "$OMARCHY_INSTALL/config/docker.sh"
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
source "$OMARCHY_INSTALL/config/all.sh"
omarchy-setup-hardware --install-user "$install_user"
run_logged "$OMARCHY_INSTALL/login/sddm.sh"
run_logged "$OMARCHY_INSTALL/post-install/pacman.sh"
run_logged "$OMARCHY_INSTALL/post-install/udev.sh"
run_logged "$OMARCHY_INSTALL/post-install/localdb.sh"
source "$OMARCHY_INSTALL/login/all.sh"
source "$OMARCHY_INSTALL/post-install/all.sh"
stop_install_log
+7
View File
@@ -40,6 +40,13 @@ fi
[[ -n ${OMARCHY_PATH:-} ]] || fail "OMARCHY_PATH is not set"
[[ -f $OMARCHY_PATH/shell/shell.qml ]] || fail "omarchy-shell config not found: $OMARCHY_PATH/shell/shell.qml"
# qs matches instances by display, and tmux run-shell strips WAYLAND_DISPLAY
# from hooks, so recover it from the compositor socket when it is missing.
if [[ -z ${WAYLAND_DISPLAY:-} ]]; then
socket=$(ls -t "${XDG_RUNTIME_DIR:-/run/user/$UID}"/wayland-[0-9]* 2>/dev/null | grep -v '\.lock$' | head -n1)
[[ -n $socket ]] && export WAYLAND_DISPLAY=${socket##*/}
fi
if [[ $1 == "shell" && ( $2 == "summon" || $2 == "toggle" ) ]] && (( $# == 3 )); then
set -- "$1" "$2" "$3" "{}"
fi
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# omarchy:summary=Shared helpers for editing ~/.config/omarchy/shell.json (source this, don't run it).
# omarchy:hidden=true
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
DEFAULTS_FILE="$OMARCHY_PATH/config/omarchy/shell.json"
fail() {
echo "${0##*/}: $*" >&2
exit 1
}
refresh_shell_config() {
if ! omarchy-shell shell reloadConfig >/dev/null 2>&1; then
omarchy-shell -q shell rescanPlugins >/dev/null 2>&1 || true
fi
}
source_file() {
if [[ -s $CONFIG_FILE ]]; then
printf '%s\n' "$CONFIG_FILE"
else
printf '%s\n' "$DEFAULTS_FILE"
fi
}
# jq pipeline that normalizes shell.json into a well-shaped object with
# version=1, bar.layout.{left,center,right} arrays, and plugins array. Every
# mutation pipes through this so downstream jq can assume structure.
NORMALIZE='
def object_or_empty: if type == "object" then . else {} end;
def array_or_empty: if type == "array" then . else [] end;
object_or_empty
| .version = 1
| .bar = (.bar | object_or_empty)
| .bar.layout = (.bar.layout | object_or_empty)
| .bar.layout.left = (.bar.layout.left | array_or_empty)
| .bar.layout.center = (.bar.layout.center | array_or_empty)
| .bar.layout.right = (.bar.layout.right | array_or_empty)
| .plugins = (.plugins | array_or_empty)
'
# Apply a jq program to the source file and atomically write the result to the
# user config, then refresh the running shell. Extra args after the program are
# forwarded to jq (e.g. --arg/--argjson).
_SHELL_CONFIG_TMP=""
cleanup_shell_config_tmp() {
if [[ -n $_SHELL_CONFIG_TMP ]]; then rm -f "$_SHELL_CONFIG_TMP"; fi
}
trap cleanup_shell_config_tmp EXIT
commit() {
local program="$1"
shift
mkdir -p "$(dirname "$CONFIG_FILE")"
_SHELL_CONFIG_TMP=$(mktemp)
jq -S -e "$@" "$program" "$(source_file)" >"$_SHELL_CONFIG_TMP" || fail "could not update shell config"
mv "$_SHELL_CONFIG_TMP" "$CONFIG_FILE"
_SHELL_CONFIG_TMP=""
refresh_shell_config
}
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# omarchy:summary=Lock and reconcile displays when the laptop lid closes
# omarchy:group=system
# omarchy:hidden=true
# Locking here rather than waiting for PrepareForSleep is what keeps the lock
# off the critical path. logind's delay inhibitor is a timer that expires
# whether or not the session is secure, so starting the lock the moment the lid
# closes gives Quickshell a head start before logind even decides to suspend.
# omarchy-system-sleep-lock then usually finds the session already secure.
#
# A docked lid close does not suspend (HandleLidSwitchDocked defaults to
# ignore), so it must not lock either: that is clamshell mode, still in use on
# the external display.
if omarchy-hw-laptop-closed && ! omarchy-hw-external-monitors; then
omarchy-system-lock >/dev/null 2>&1 || true
fi
omarchy-hyprland-monitor-clamshell
+1 -1
View File
@@ -6,7 +6,7 @@
nohup bash -c "sleep 2 && uwsm stop" >/dev/null 2>&1 &
omarchy-osd -i logout -m "Logging out" -d 5000 --fit
omarchy-osd -i logout -m "Logging out" -d 5000
# Now close all windows
omarchy-hyprland-window-close-all
+1 -1
View File
@@ -8,7 +8,7 @@
# scope cannot terminate it before it runs.
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl reboot --no-wall || exit 1
omarchy-osd -i reboot -m "Rebooting" -d 5000 --fit
omarchy-osd -i reboot -m "Rebooting" -d 5000
omarchy-state clear re*-required
+1 -1
View File
@@ -8,7 +8,7 @@
# scope cannot terminate it before it runs.
systemd-run --user --collect --quiet --on-active="2s" --timer-property=AccuracySec=100ms systemctl poweroff --no-wall || exit 1
omarchy-osd -i shutdown -m "Shutting down" -d 5000 --fit
omarchy-osd -i shutdown -m "Shutting down" -d 5000
omarchy-state clear re*-required
+109 -17
View File
@@ -4,29 +4,121 @@
# omarchy:group=system
# omarchy:hidden=true
wait_attempts=${1:-100}
# Overrunning the budget is the failure this whole path exists to prevent:
# logind stops honouring the inhibitor and suspends mid-lock. Every call below
# is bounded by what is left of the budget, so the deadline enforces itself
# rather than depending on an estimate of how long a step ought to take.
budget_cap_ms=12000
lock_timeout_ms=1000
status_timeout_ms=500
poll_interval=0.1
if [[ ! $wait_attempts =~ ^[0-9]+$ ]] || (( wait_attempts < 1 )); then
wait_attempts=100
fi
# logind decides how long a delay inhibitor may hold the machine, and the
# shipped drop-in only counts once logind has reloaded it, so ask rather than
# assume. Leaving logind a fifth of its own window to deliver PrepareForSleep
# and act on the release gives 4s at the 5s default and 12s at the shipped 15s.
# The cap keeps a hand-raised window from stranding a closed laptop in a bag.
derive_budget_ms() {
local window
window=$(timeout --kill-after=0.1s 1s busctl get-property \
org.freedesktop.login1 /org/freedesktop/login1 \
org.freedesktop.login1.Manager InhibitDelayMaxUSec 2>/dev/null)
window=${window##* }
sync_clamshell() {
omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true
# An unreadable window means we cannot know, so assume logind's own default.
[[ $window =~ ^[0-9]+$ ]] && (( window > 0 )) || window=5000000
window=$((window / 1000))
# Never leave logind less than a second, however small its window is.
window=$((window - (window / 5 > 1000 ? window / 5 : 1000)))
(( window < budget_cap_ms )) && echo "$window" || echo "$budget_cap_ms"
}
budget_ms=${1:-$(derive_budget_ms)}
if [[ ! $budget_ms =~ ^[0-9]+$ ]] || (( budget_ms < 1 || budget_ms > budget_cap_ms )); then
budget_ms=$(derive_budget_ms)
fi
# EPOCHREALTIME renders with the locale's decimal separator, so drop every
# non-digit rather than assuming a period. A comma would otherwise read as
# bash's comma operator and silently void the deadline.
deadline_ms=$((10#${EPOCHREALTIME//[!0-9]/} / 1000 + budget_ms))
remaining_ms() {
echo $((deadline_ms - 10#${EPOCHREALTIME//[!0-9]/} / 1000))
}
# Clamping to what is left as well as to the call's own limit is what lets the
# loop below stay a plain "while there is time" without predicting step costs.
lock_ipc() {
local limit=$1 remaining seconds
shift
remaining=$(remaining_ms)
(( remaining > 0 )) || return 1
(( limit < remaining )) || limit=$remaining
printf -v seconds '%d.%03d' $((limit / 1000)) $((limit % 1000))
OMARCHY_SHELL_IPC_TIMEOUT="$seconds" \
timeout --kill-after=0.1s "$seconds" omarchy-shell "$@"
}
# The shell answers refusals on stdout with a zero exit, so the reply is the
# only way to spot a lock it can never perform. Nothing else needs inspecting:
# the status poll is what confirms success, and re-requesting is idempotent, so
# a request that may not have landed costs nothing to repeat.
request_lock() {
case $(lock_ipc "$lock_timeout_ms" lock lock 2>/dev/null) in
missing-pam) report_unsecured "no lock screen is configured" ;;
esac
}
# secure: done. locking: the shell has the request and is working on it, so
# leave it alone. Anything else, unreadable replies included, means ask again.
lock_state() {
jq -r 'if .secure == true then "secure"
elif .requested == true then "locking"
else "idle" end' \
<<<"$(lock_ipc "$status_timeout_ms" lock status 2>/dev/null)" 2>/dev/null
}
sync_clamshell() {
# Lid transitions can temporarily stall Hyprland IPC. This is best-effort:
# the lid binding and monitor watcher also reconcile clamshell state.
timeout --kill-after=0.1s 0.4s \
omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true
}
# logind suspends whether or not this wait succeeded, so a failure here means
# the machine slept with the session exposed. The notification is the only way
# anyone finds out, and it lands on the screen they unlock into.
report_unsecured() {
printf 'omarchy-system-sleep-lock: suspending without a secure lock (%s)\n' \
"$1" >&2
omarchy-notification-send -u critical -g 󰌾 \
"Screen did not lock before suspend" \
"The session was left unlocked ($1)." >/dev/null 2>&1 || true
exit 1
}
# Request the lock before touching monitor state, so a stuck Hyprland IPC call
# cannot consume the window before Quickshell has begun securing the session.
request_lock
sync_clamshell
omarchy-shell lock lock >/dev/null 2>&1 || exit 1
for (( attempt = 0; attempt < wait_attempts; attempt++ )); do
(( attempt % 5 == 0 )) && sync_clamshell
# The trailing sleep can overshoot the deadline by one interval, which is well
# inside the reserve derive_budget_ms already held back for logind.
while (( $(remaining_ms) > 0 )); do
case $(lock_state) in
secure) exit 0 ;;
locking) ;;
*) request_lock ;;
esac
status=$(omarchy-shell lock status 2>/dev/null || true)
if jq -e '.secure == true' <<<"$status" >/dev/null 2>&1; then
exit 0
fi
sleep 0.05
sleep "$poll_interval"
done
exit 1
report_unsecured "the shell did not secure the session within ${budget_ms}ms"
+1 -1
View File
@@ -26,7 +26,7 @@ usage() {
echo "Usage: omarchy-theme-color [--file <colors.toml>] (--all | --raw | <key> [fallback])"
}
while [[ $# -gt 0 ]]; do
while (( $# > 0 )); do
case "$1" in
--file)
COLORS_FILE="${2:-}"
+1 -1
View File
@@ -6,7 +6,7 @@
THEME_NAME_PATH="$HOME/.local/state/omarchy/current/theme.name"
if [[ -f $THEME_NAME_PATH ]]; then
cat $THEME_NAME_PATH | sed -E 's/(^|-)([a-z])/\1\u\2/g; s/-/ /g'
sed -E 's/(^|-)([a-z])/\1\u\2/g; s/-/ /g' "$THEME_NAME_PATH"
else
echo "Unknown"
fi
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# omarchy:summary=Print the directory holding a theme, preferring a user-installed copy
# omarchy:args=<theme-name>
# omarchy:examples=omarchy theme dir tokyo-night
theme="${1:-}"
if [[ -z $theme ]]; then
echo "Usage: omarchy-theme-dir <theme-name>" >&2
exit 1
fi
if [[ -d $HOME/.config/omarchy/themes/$theme ]]; then
echo "$HOME/.config/omarchy/themes/$theme"
else
echo "$OMARCHY_PATH/themes/$theme"
fi
+1 -3
View File
@@ -6,6 +6,4 @@
{
find ~/.config/omarchy/themes/ -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -printf '%f\n'
find "$OMARCHY_PATH/themes/" -mindepth 1 -maxdepth 1 -type d -printf '%f\n'
} | sort -u | while read -r name; do
echo "$name" | sed -E 's/(^|-)([a-z])/\1\u\2/g; s/-/ /g'
done
} | sort -u | sed -E 's/(^|-)([a-z])/\1\u\2/g; s/-/ /g'
+140
View File
@@ -0,0 +1,140 @@
#!/bin/bash
# omarchy:summary=Show or jump to tmux windows waiting for attention
# omarchy:args=<show [--json]|focus>
# omarchy:examples=omarchy tmux alert show | omarchy tmux alert focus
set -e
# Session names cannot contain colons, and window names can, so they always
# come last when parsed.
WINDOW_FORMAT='#{window_id}:#{window_bell_flag}#{window_activity_flag}#{window_silence_flag}:#{window_activity}:#{@omarchy_unfocused_activity}:#{session_name}:#{window_index}:#{window_name}'
CLIENT_FORMAT='#{client_flags}:#{window_id}'
usage() {
echo "Usage: omarchy-tmux-alert <show [--json]|focus>" >&2
exit 1
}
# Include tmux alert flags plus output newer than the last focus transition for
# selected windows whose attached terminals are all unfocused.
alerted_windows() {
local client_flags window_id flags activity seen_activity session index name
local waiting
local -A attached_windows=()
local -A focused_windows=()
while IFS=':' read -r client_flags window_id; do
[[ -n $window_id ]] || continue
attached_windows[$window_id]=1
if [[ ,$client_flags, == *",focused,"* ]]; then
focused_windows[$window_id]=1
fi
done < <(tmux list-clients -F "$CLIENT_FORMAT" 2>/dev/null)
while IFS=':' read -r window_id flags activity seen_activity session index name; do
waiting=0
[[ $flags == *1* ]] && waiting=1
if [[ ${attached_windows[$window_id]:-} == "1" &&
${focused_windows[$window_id]:-} != "1" &&
$activity =~ ^[0-9]+$ &&
$seen_activity =~ ^[0-9]+$ ]] && ((activity > seen_activity)); then
waiting=1
fi
((waiting)) && printf '%s:%s:%s\n' "$session" "$index" "$name"
done < <(tmux list-windows -a -F "$WINDOW_FORMAT" 2>/dev/null)
}
show() {
local windows=() window session index name description=""
readarray -t windows < <(alerted_windows)
for window in "${windows[@]}"; do
IFS=':' read -r session index name <<<"$window"
[[ -n $description ]] && description+=", "
description+="$name ($session:$index)"
done
if [[ ${1:-} == "--json" ]]; then
jq -nc --argjson count "${#windows[@]}" --arg tooltip "$description" '{count: $count, tooltip: $tooltip}'
elif [[ -n $description ]]; then
echo "$description"
fi
}
focus_terminal_window() {
local pid=$1 address="" window_pid window_address
local -A windows=()
while read -r window_pid window_address; do
windows[$window_pid]=$window_address
done < <(hyprctl clients -j | jq -r '.[] | "\(.pid) \(.address)"')
# The tmux client runs somewhere below the terminal that owns the window.
while [[ -n $pid ]] && (( pid > 1 )); do
if [[ -n ${windows[$pid]:-} ]]; then
address=${windows[$pid]}
break
fi
pid=$(awk '/^PPid:/ { print $2 }' "/proc/$pid/status" 2>/dev/null)
done
[[ -n $address ]] || return 1
hyprctl dispatch "hl.dsp.focus({ window = \"address:$address\" })" >/dev/null 2>&1 ||
hyprctl dispatch focuswindow "address:$address" >/dev/null
}
most_recent_client() {
tmux list-clients "$@" -F '#{client_activity}:#{client_tty}:#{client_pid}' 2>/dev/null | sort -rn | head -n1
}
track() {
local window=${1:-} activity=${2:-}
[[ $window =~ ^@[0-9]+$ && $activity =~ ^[0-9]+$ ]] || return 0
tmux set-option -wq -t "$window" @omarchy_unfocused_activity "$activity" 2>/dev/null || return 0
omarchy-shell -q omarchy.indicators refresh
}
focus() {
local target session index client tty pid
target=$(alerted_windows | head -n1)
[[ -n $target ]] || return 0
IFS=':' read -r session index _ <<<"$target"
client=$(most_recent_client -t "$session")
[[ -n $client ]] || client=$(most_recent_client)
if [[ -z $client ]]; then
tmux select-window -t "$session:$index"
exec omarchy-launch-terminal tmux attach -t "$session"
fi
IFS=':' read -r _ tty pid <<<"$client"
tmux switch-client -c "$tty" -t "$session:$index"
omarchy-shell -q omarchy.indicators refresh
focus_terminal_window "$pid"
}
case "${1:-}" in
show)
shift
show "$@"
;;
focus)
focus
;;
track)
shift
track "$@"
;;
*)
usage
;;
esac
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# omarchy:summary=Enable, disable, or toggle a Hyprland input device
# omarchy:args=<touchpad|touchscreen> [on|off|toggle]
# omarchy:hidden=true
KIND="${1:-}"
ACTION="${2:-toggle}"
case "$KIND" in
touchpad) LABEL="Touchpad" ICON="touchpad" ;;
touchscreen) LABEL="Touchscreen" ICON="touch" ;;
*)
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
exit 1
;;
esac
# Hyprland sources this directory on reload, so the disabled state survives restarts
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/$KIND-disabled.lua"
device="$("omarchy-hw-$KIND")"
if [[ -z $device ]]; then
echo "No $KIND device found" >&2
exit 1
fi
enable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
rm -f "$STATE_FILE"
omarchy-osd -i "$ICON" -m "$LABEL enabled"
}
disable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
mkdir -p "$(dirname "$STATE_FILE")"
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
omarchy-osd -i "$ICON" -m "$LABEL disabled"
}
case "$ACTION" in
on) enable ;;
off) disable ;;
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
*)
echo "Usage: omarchy-toggle-input-device <touchpad|touchscreen> [on|off|toggle]" >&2
exit 1
;;
esac
+4 -4
View File
@@ -2,10 +2,10 @@
# omarchy:summary=Toggle screensaver availability
omarchy-toggle screensaver-off
if omarchy-toggle-enabled screensaver-off; then
omarchy-toggle screensaver-off
omarchy-notification-send -g 󱄄 "Screensaver enabled"
else
omarchy-toggle screensaver-off
omarchy-notification-send -g 󱄄 "Screensaver disabled"
else
omarchy-notification-send -g 󱄄 "Screensaver enabled"
fi
+4 -4
View File
@@ -2,10 +2,10 @@
# omarchy:summary=Toggle suspend availability in the system menu
omarchy-toggle suspend-off
if omarchy-toggle-enabled suspend-off; then
omarchy-toggle suspend-off
omarchy-notification-send -g 󰒲 "Suspend now available in system menu"
else
omarchy-toggle suspend-off
omarchy-notification-send -g 󰒲 "Suspend removed from system menu"
else
omarchy-notification-send -g 󰒲 "Suspend now available in system menu"
fi
+1 -27
View File
@@ -3,30 +3,4 @@
# omarchy:summary=Enable, disable, or toggle the touchpad
# omarchy:args=[on|off|toggle]
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/touchpad-disabled.lua"
device="$(omarchy-hw-touchpad)"
if [[ -z $device ]]; then
echo "No touchpad device found" >&2
exit 1
fi
enable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
rm -f "$STATE_FILE"
omarchy-osd -i touchpad -m "Touchpad enabled"
}
disable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
mkdir -p "$(dirname "$STATE_FILE")"
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
omarchy-osd -i touchpad -m "Touchpad disabled"
}
case "${1:-toggle}" in
on) enable ;;
off) disable ;;
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
esac
exec omarchy-toggle-input-device touchpad "${1:-toggle}"
+1 -27
View File
@@ -3,30 +3,4 @@
# omarchy:summary=Enable, disable, or toggle the touch functionality of the screen
# omarchy:args=[on|off|toggle]
STATE_FILE="$HOME/.local/state/omarchy/toggles/hypr/touchscreen-disabled.lua"
device="$(omarchy-hw-touchscreen)"
if [[ -z $device ]]; then
echo "No touchscreen device found" >&2
exit 1
fi
enable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = true })" >/dev/null
rm -f "$STATE_FILE"
omarchy-osd -i touch -m "Touchscreen enabled"
}
disable() {
hyprctl eval "hl.device({ name = \"$device\", enabled = false })" >/dev/null
mkdir -p "$(dirname "$STATE_FILE")"
printf 'hl.device({ name = "%s", enabled = false })\n' "$device" >"$STATE_FILE"
omarchy-osd -i touch -m "Touchscreen disabled"
}
case "${1:-toggle}" in
on) enable ;;
off) disable ;;
toggle) if [[ -f $STATE_FILE ]]; then enable; else disable; fi ;;
esac
exec omarchy-toggle-input-device touchscreen "${1:-toggle}"
+7
View File
@@ -73,6 +73,7 @@ run_update_pipeline() {
disable_sleep_for_update
disable_idle_for_update
omarchy-update-dev
omarchy-update-keyring
omarchy-update-system-pkgs
omarchy-migrate
@@ -90,6 +91,12 @@ run_update_pipeline() {
omarchy-shell -q omarchy.system-update clear
fi
# Release update-owned inhibitors before offering a reboot. A confirmed
# reboot can terminate this process before its EXIT trap gets a chance to
# remove the persistent Stay Awake marker.
restore_update_inhibitors
trap - EXIT
omarchy-update-restart
}
+22 -2
View File
@@ -1,9 +1,25 @@
#!/bin/bash
# omarchy:summary=Check whether Omarchy package updates are available.
# omarchy:summary=Check whether Omarchy updates are available.
set -euo pipefail
updates=()
if [[ $OMARCHY_PATH != "/usr/share/omarchy" ]]; then
upstream=$(git -C "$OMARCHY_PATH" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null || true)
if [[ -n $upstream ]]; then
GIT_TERMINAL_PROMPT=0 timeout 10 git -C "$OMARCHY_PATH" fetch --quiet 2>/dev/null || true
behind=$(git -C "$OMARCHY_PATH" rev-list --count "HEAD..$upstream" 2>/dev/null || echo 0)
if (( behind > 0 )); then
commit_label=commits
(( behind == 1 )) && commit_label=commit
updates+=("omarchy-dev-checkout $behind new $commit_label on $upstream")
fi
fi
fi
package=""
if pacman -Qq omarchy-dev >/dev/null 2>&1; then
package=omarchy-dev
@@ -16,7 +32,11 @@ if [[ -n $package ]]; then
fi
if [[ -n ${update:-} ]]; then
printf '%s\n' "$update"
updates+=("$update")
fi
if (( ${#updates[@]} > 0 )); then
printf '%s\n' "${updates[@]}"
exit 0
else
echo "Omarchy is up to date"
+21
View File
@@ -0,0 +1,21 @@
#!/bin/bash
# omarchy:summary=Update the active Omarchy dev checkout
set -euo pipefail
[[ $OMARCHY_PATH != "/usr/share/omarchy" ]] || exit 0
if ! git -C "$OMARCHY_PATH" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Error: OMARCHY_PATH is not a git checkout: $OMARCHY_PATH" >&2
exit 1
fi
upstream=$(git -C "$OMARCHY_PATH" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null || true)
if [[ -z $upstream ]]; then
echo -e "\e[33m\nSkip Omarchy dev checkout update (current branch has no upstream)\e[0m"
exit 0
fi
echo -e "\e[32m\nUpdate Omarchy dev checkout\e[0m"
git -C "$OMARCHY_PATH" pull --ff-only
+4
View File
@@ -11,6 +11,10 @@ echo -e "\e[32m\nUpdate system packages\e[0m"
# as unowned files; pacman would refuse the omarchy-settings upgrade on first
# encounter. Drop each entry once the transition release is the baseline.
#
# An entry has to ship at least one release before the package starts owning the
# path. pacman checks file conflicts during transaction prepare, so the copy of
# this script running the upgrade is the one already on disk.
#
# The /usr/share/omarchy/* entry is permanent: that tree is wholly owned by
# the omarchy packages, but files can land there unowned (in-place extension
# work, script-written files), which would abort the whole upgrade. Packaged
+1 -2
View File
@@ -1492,7 +1492,6 @@ always_copy_config_files=(
hypr/input.lua
hypr/looknfeel.lua
hypr/monitors.lua
omarchy/bar.json
omarchy/extensions/omarchy-menu.jsonc
omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
omarchy/shell.json
@@ -2128,7 +2127,7 @@ if command -v update-desktop-database >/dev/null 2>&1; then
update-desktop-database "$HOME/.local/share/applications" >/dev/null 2>&1 || true
fi
if command -v xdg-settings >/dev/null 2>&1; then
xdg-settings set default-web-browser chromium.desktop || true
env -u BROWSER xdg-settings set default-web-browser chromium.desktop || true
fi
if command -v xdg-mime >/dev/null 2>&1; then
xdg-mime default HEY.desktop x-scheme-handler/mailto || true
-10
View File
@@ -1,10 +0,0 @@
#!/bin/bash
# omarchy:summary=Set Wi-Fi power save mode on wireless interfaces
# omarchy:args=<on|off>
shopt -s nullglob
for iface in /sys/class/net/*/wireless; do
iface="$(basename "$(dirname "$iface")")"
iw dev "$iface" set power_save "$1" 2>/dev/null
done
+1 -1
View File
@@ -26,4 +26,4 @@
-- Logitech MX Keys examples:
-- o.bind("SUPER + SHIFT + S", nil, "omarchy-capture-screenshot")
-- o.bind("SUPER + H", nil, "voxtype record toggle")
-- o.bind("SUPER + PERIOD", nil, { omarchy = "walker -m symbols" })
-- o.bind("SUPER + PERIOD", nil, "omarchy-shell shell toggle omarchy.emojis")
-1
View File
@@ -1 +0,0 @@
{}
+3 -3
View File
@@ -18,6 +18,9 @@
}
],
"center": [
{
"id": "omarchy.indicators"
},
{
"id": "omarchy.clock",
"format": "dddd HH:mm",
@@ -29,9 +32,6 @@
},
{
"id": "omarchy.system-update"
},
{
"id": "omarchy.indicators"
}
],
"right": [
+9
View File
@@ -81,6 +81,15 @@ set -ag terminal-features "xterm-kitty:extkeys"
set -as terminal-features ",*:clipboard"
set -sg escape-time 10
# Alerts
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g alert-activity 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g alert-silence 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
set-hook -g after-select-window 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
set-hook -g client-session-changed 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
set-hook -g client-focus-out[100] 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
set-hook -g client-focus-in[100] 'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'
# Status bar
set -g status-position top
set -g status-interval 5
+40
View File
@@ -0,0 +1,40 @@
# Host config for the Omarchy speaker tuning.
#
# This exists so the tuning gets its own PipeWire client rather than sharing
# PipeWire's stock filter-chain.conf. That config merges every fragment in
# ~/.config/pipewire/filter-chain.conf.d/, so hosting the tuning there would load
# any unrelated filter a user keeps in that directory -- duplicating filters
# already hosted elsewhere, and stopping them all when the tuning is switched off.
#
# Installed as ~/.config/pipewire/omarchy-speaker-tuning.conf with the tuning
# graph merged from omarchy-speaker-tuning.conf.d/, and run with
# pipewire -c omarchy-speaker-tuning.conf
#
# The contents are the minimum a filter-hosting client needs, taken from
# /usr/share/pipewire/filter-chain.conf.
context.properties = {
log.level = 0
}
context.spa-libs = {
audio.convert.* = audioconvert/libspa-audioconvert
support.* = support/libspa-support
}
context.modules = [
# Boost the audio thread priority.
{ name = libpipewire-module-rt
args = { }
flags = [ ifexists nofail ]
}
# The native communication protocol.
{ name = libpipewire-module-protocol-native }
# Lets this process provide nodes to PipeWire.
{ name = libpipewire-module-client-node }
# Wraps nodes in an adapter with a converter and resampler.
{ name = libpipewire-module-adapter }
]
@@ -0,0 +1,140 @@
# Dell XPS 14 / XPS 16 (2026) speaker tuning.
#
# Biquad chain fitted to the measured response of the xps-audio-linux EasyEffects
# profile under a dense pink-weighted multitone of 104 bin-aligned tones,
# followed by a lookahead limiter. Measures 1.24 dB RMS against that reference
# (0.97 dB weighted over the fit's own error metric).
#
# Fitted and measured on the XPS 14 (SKU 0DB9); the XPS 16 (0DBA) is covered on
# report that the same profile suits it. See tuning.conf.
#
# Q below 200 Hz is capped at 1.8 on purpose. A closer magnitude fit is possible
# with high-Q sections, but the reference produces its narrow bass features by
# convolution, and reproducing them with high-Q biquads swung group delay 31 ms
# across 63-80 Hz, which smears bass transients. The cap costs 0.33 dB and
# halves the swing.
#
# This is a plain filter-chain sink rather than a WirePlumber smart filter. A
# smart filter is the better shape -- it would leave the real device as the
# default output instead of adding a second one -- but on PipeWire 1.6.8 /
# WirePlumber 0.5.15 this graph loads and links correctly as a smart filter and
# then passes audio through unprocessed: its controls are present and
# mpv -> filter -> sink links are made, yet the filter's input monitor and the
# speaker sink's monitor measure identically. Revisit when that is understood.
#
# Channels are wired explicitly because the limiter is a stereo plugin; a mono
# graph is duplicated per channel and would limit each side independently,
# shifting the stereo image on bass transients.
context.modules = [
{ name = libpipewire-module-filter-chain
args = {
node.description = "Laptop Speakers"
media.name = "Laptop Speakers"
filter.graph = {
nodes = [
{ type = builtin name = s0_l label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
{ type = builtin name = s1_l label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
{ type = builtin name = s2_l label = bq_peaking control = { "Freq" = 83.4 "Q" = 1.8 "Gain" = -8.0 } }
{ type = builtin name = s3_l label = bq_peaking control = { "Freq" = 100.4 "Q" = 1.59 "Gain" = 7.47 } }
{ type = builtin name = s4_l label = bq_peaking control = { "Freq" = 250.5 "Q" = 2.966 "Gain" = -4.7 } }
{ type = builtin name = s5_l label = bq_peaking control = { "Freq" = 419.8 "Q" = 3.0 "Gain" = -5.83 } }
{ type = builtin name = s6_l label = bq_peaking control = { "Freq" = 631.3 "Q" = 2.515 "Gain" = -10.33 } }
{ type = builtin name = s7_l label = bq_peaking control = { "Freq" = 894.4 "Q" = 4.0 "Gain" = -2.42 } }
{ type = builtin name = s8_l label = bq_peaking control = { "Freq" = 1355.7 "Q" = 2.884 "Gain" = 6.92 } }
{ type = builtin name = s9_l label = bq_peaking control = { "Freq" = 1707.2 "Q" = 1.311 "Gain" = -6.54 } }
{ type = builtin name = s10_l label = bq_peaking control = { "Freq" = 3100.0 "Q" = 0.5 "Gain" = -10.09 } }
{ type = builtin name = s11_l label = bq_peaking control = { "Freq" = 3200.0 "Q" = 1.048 "Gain" = 3.09 } }
{ type = builtin name = s12_l label = bq_highshelf control = { "Freq" = 6015.2 "Q" = 1.5 "Gain" = -1.34 } }
{ type = builtin name = s0_r label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
{ type = builtin name = s1_r label = bq_highpass control = { "Freq" = 60.9 "Q" = 1.0 } }
{ type = builtin name = s2_r label = bq_peaking control = { "Freq" = 83.4 "Q" = 1.8 "Gain" = -8.0 } }
{ type = builtin name = s3_r label = bq_peaking control = { "Freq" = 100.4 "Q" = 1.59 "Gain" = 7.47 } }
{ type = builtin name = s4_r label = bq_peaking control = { "Freq" = 250.5 "Q" = 2.966 "Gain" = -4.7 } }
{ type = builtin name = s5_r label = bq_peaking control = { "Freq" = 419.8 "Q" = 3.0 "Gain" = -5.83 } }
{ type = builtin name = s6_r label = bq_peaking control = { "Freq" = 631.3 "Q" = 2.515 "Gain" = -10.33 } }
{ type = builtin name = s7_r label = bq_peaking control = { "Freq" = 894.4 "Q" = 4.0 "Gain" = -2.42 } }
{ type = builtin name = s8_r label = bq_peaking control = { "Freq" = 1355.7 "Q" = 2.884 "Gain" = 6.92 } }
{ type = builtin name = s9_r label = bq_peaking control = { "Freq" = 1707.2 "Q" = 1.311 "Gain" = -6.54 } }
{ type = builtin name = s10_r label = bq_peaking control = { "Freq" = 3100.0 "Q" = 0.5 "Gain" = -10.09 } }
{ type = builtin name = s11_r label = bq_peaking control = { "Freq" = 3200.0 "Q" = 1.048 "Gain" = 3.09 } }
{ type = builtin name = s12_r label = bq_highshelf control = { "Freq" = 6015.2 "Q" = 1.5 "Gain" = -1.34 } }
{ type = lv2
name = limiter
plugin = "http://lsp-plug.in/plugins/lv2/limiter_stereo"
control = {
# Both default to enabled: "alr" regulates level toward the
# threshold and "boost" normalises the threshold up to full
# scale. A fixed tuning must switch them off or its tone drifts
# with programme level.
"alr" = 0
"boost" = 0
"g_in" = 0.5456
"th" = 0.891
}
}
]
links = [
{ output = "s0_l:Out" input = "s1_l:In" }
{ output = "s1_l:Out" input = "s2_l:In" }
{ output = "s2_l:Out" input = "s3_l:In" }
{ output = "s3_l:Out" input = "s4_l:In" }
{ output = "s4_l:Out" input = "s5_l:In" }
{ output = "s5_l:Out" input = "s6_l:In" }
{ output = "s6_l:Out" input = "s7_l:In" }
{ output = "s7_l:Out" input = "s8_l:In" }
{ output = "s8_l:Out" input = "s9_l:In" }
{ output = "s9_l:Out" input = "s10_l:In" }
{ output = "s10_l:Out" input = "s11_l:In" }
{ output = "s11_l:Out" input = "s12_l:In" }
{ output = "s12_l:Out" input = "limiter:in_l" }
{ output = "s0_r:Out" input = "s1_r:In" }
{ output = "s1_r:Out" input = "s2_r:In" }
{ output = "s2_r:Out" input = "s3_r:In" }
{ output = "s3_r:Out" input = "s4_r:In" }
{ output = "s4_r:Out" input = "s5_r:In" }
{ output = "s5_r:Out" input = "s6_r:In" }
{ output = "s6_r:Out" input = "s7_r:In" }
{ output = "s7_r:Out" input = "s8_r:In" }
{ output = "s8_r:Out" input = "s9_r:In" }
{ output = "s9_r:Out" input = "s10_r:In" }
{ output = "s10_r:Out" input = "s11_r:In" }
{ output = "s11_r:Out" input = "s12_r:In" }
{ output = "s12_r:Out" input = "limiter:in_r" }
]
inputs = [ "s0_l:In" "s0_r:In" ]
outputs = [ "limiter:out_l" "limiter:out_r" ]
}
audio.channels = 2
audio.position = [ FL FR ]
capture.props = {
node.name = "omarchy_speaker_tuning"
media.class = Audio/Sink
}
playback.props = {
node.name = "omarchy_speaker_tuning_output"
node.passive = true
target.object = "@SPEAKER_SINK@"
# This stream is the filter's output and is a movable sink input like any
# other, so anything that reroutes "all streams" to a newly selected
# output would drag the processing along with it -- onto headphones, or
# into the tuning's own sink, which is a cycle. Pin it.
node.dont-move = true
# If the speaker sink is not present yet -- the tuning host can start
# before the device is discovered -- WirePlumber would otherwise link this
# output to whatever default exists, quietly tuning the wrong device while
# the tuning sink still looks healthy. Wait for the named target instead.
# Both are needed: without linger, WirePlumber destroys the node rather
# than waiting (see its scripts/linking/find-defined-target.lua).
node.dont-fallback = true
node.linger = true
}
}
}
]
@@ -0,0 +1,39 @@
## Dell XPS 14 / XPS 16 (2026) internal speakers.
##
## Thirteen biquads and a lookahead limiter, applied as a PipeWire filter-chain
## in front of the internal speaker sink. The stock Linux path already loads
## Dell's Cirrus smart-amplifier firmware; this adds the perceptual voicing the
## Windows Waves layer provides and Linux does not.
description="Dell XPS 14/16 (2026) speakers"
## Matched on the DMI product SKU, which is what Dell keys the Cirrus speaker
## firmware on -- 10280db9 for the XPS 14 and 10280dba for the XPS 16 -- so it
## identifies the speaker hardware itself rather than a marketing name. Compared as
## whole values, so this cannot widen to the rest of the XPS line.
##
## 0DB9 XPS 14 -- measured here, see below
## 0DBA XPS 16 -- included on report that this profile suits it, not measured
match_sku=("0DB9" "0DBA")
## Unescaped dots: this is passed to awk as a string, where a backslash escape
## would be consumed before the regex sees it.
sink_pattern='^alsa_output.*sof_sdw.*HiFi__Speaker__sink$'
## Provenance. Derived by measuring the response of the xps-clone EasyEffects
## profile from https://github.com/spencerbull/xps-audio-linux (MIT) and fitting
## a biquad chain to it. No upstream asset is redistributed: the convolution
## impulse response is not carried, so this tuning has no binary blob and is
## sample-rate agnostic.
derived_from="xps-audio-linux xps-clone (MIT, spencerbull)"
validated_by="dhh"
validated_on="2026-07-24"
## The measurements below were taken on the XPS 14 (0DB9). The XPS 16 (0DBA) is
## covered on report rather than measurement; re-measure there before treating
## these figures as describing it.
validated_hardware="XPS 14 DA14260 (0DB9)"
## Measured against that reference under a dense pink-weighted multitone of 104
## bin-aligned tones. See docs/AUDIO-TUNING.md for how to reproduce these.
magnitude_rms_db="1.24"
bass_group_delay_swing_ms="13.2"
limiter_headroom_db="1.6" ## worst-case peak on a hot master vs -1 dBFS
dynamic_range_delta_lu="0.1"
@@ -38,6 +38,34 @@
</edit>
</match>
<!-- Urdu is conventionally set in Nastaliq, so keep it there. Without this
the untargeted Naskh rule below would capture Urdu too. Appended rather
than prepended so it only wins the last-resort race against that rule
and never displaces the family the app actually asked for. -->
<match target="pattern">
<test name="lang" compare="contains">
<string>ur</string>
</test>
<edit name="family" mode="append" binding="strong">
<string>Noto Nastaliq Urdu</string>
</edit>
</match>
<!-- Chromium and Electron resolve missing glyphs one character at a time
without lang on the pattern, so the rules above never fire for them and
fontconfig falls through to a raw charset scan that lands on Nastaliq
(or on Kufi, once the latin alias chains have widened the pattern).
Naming Naskh as a last-resort family covers that path. Appending keeps
it behind every family the app actually asked for, and charset matching
outranks family matching, so a font that does not cover the codepoint
can never be pulled in: Latin, monospace, emoji, Nerd Font glyphs and
CJK all resolve exactly as before. -->
<match target="pattern">
<edit name="family" mode="append" binding="strong">
<string>Noto Naskh Arabic</string>
</edit>
</match>
<alias>
<family>system-ui</family>
<prefer>
+8 -2
View File
@@ -1,2 +1,8 @@
-- DaVinci Resolve dialog focus handling.
o.window(".*[Rr]esolve.*", { float = true, stay_focused = true })
-- DaVinci Resolve window focus handling. Kept fully opaque: the default
-- translucency distorts colour-critical grading work.
o.window(".*[Rr]esolve.*", {
float = true,
stay_focused = true,
tag = "-default-opacity",
opacity = "1 1",
})
+1 -1
View File
@@ -7,7 +7,7 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation
-- Launcher, image selector, emojis, clipboard overlays, and keyboard-driven
-- panels should pop without compositor layer fades. Panels keep their own
-- QML opacity transition for normal open/close, and skip it for panel handoff.
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-launcher|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel)$" }, no_anim = true, animation = "none" })
hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel)$" }, no_anim = true, animation = "none" })
-- Dev gallery is the main shell workbench; open it maximized like
-- SUPER+ALT+F so component previews have the whole workspace.
@@ -1,2 +1,2 @@
-- Remove 1px border around hyprshot screenshots.
-- Remove the 1px border around the slurp region selection used by screenshots.
hl.layer_rule({ match = { namespace = "selection" }, no_anim = true, animation = "none" })
+23 -29
View File
@@ -7,33 +7,27 @@ o.bind("SUPER + SHIFT + B", "Browser", { omarchy = "browser" })
o.bind("SUPER + SHIFT + ALT + B", "Browser (private)", { omarchy = "browser --private" })
o.bind("SUPER + SHIFT + N", "Editor", { omarchy = "editor" })
if not o.preinstalled_bindings_enabled() then
return
if o.preinstalled_bindings_enabled() then
-- Bindings for preinstalled Omarchy applications, TUIs, and web apps.
o.bind("SUPER + ALT + RETURN", "Tmux", { omarchy = "terminal-tmux" })
o.bind("SUPER + SHIFT + M", "Music", { omarchy = "spotify" })
o.bind("SUPER + SHIFT + ALT + M", "Music TUI", { tui = "cliamp", focus = true })
o.bind("SUPER + SHIFT + D", "Docker", { tui = "lazydocker" })
o.bind("SUPER + SHIFT + G", "Signal", { omarchy = "signal" })
o.bind("SUPER + SHIFT + O", "Obsidian", { launch = "obsidian", focus = "^obsidian$" })
o.bind("SUPER + SHIFT + W", "Omawrite", { launch = "omawrite" })
o.bind("SUPER + SHIFT + SLASH", "Passwords", { omarchy = "1password" })
o.bind("SUPER + SHIFT + A", "ChatGPT", { webapp = "https://chatgpt.com" })
o.bind("SUPER + SHIFT + ALT + A", "Grok", { webapp = "https://grok.com" })
o.bind("SUPER + SHIFT + C", "Calendar", { webapp = "https://app.hey.com/calendar/weeks/" })
o.bind("SUPER + SHIFT + E", "Email", { webapp = "https://app.hey.com" })
o.bind("SUPER + SHIFT + ALT + E", "New email", { webapp = "https://app.hey.com/messages/new?display=standalone&new_window=true" })
o.bind("SUPER + SHIFT + Y", "YouTube", { webapp = "https://youtube.com/" })
o.bind("SUPER + SHIFT + ALT + G", "WhatsApp", { webapp = "https://web.whatsapp.com/", focus = true })
o.bind( "SUPER + SHIFT + CTRL + G", "Google Messages", { webapp = "https://messages.google.com/web/conversations", focus = true })
o.bind("SUPER + SHIFT + P", "Google Photos", { webapp = "https://photos.google.com/", focus = true })
o.bind("SUPER + SHIFT + S", "Google Maps", { webapp = "https://maps.google.com/", focus = true })
o.bind("SUPER + SHIFT + X", "X", { webapp = "https://x.com/" })
o.bind("SUPER + SHIFT + ALT + X", "X Post", { webapp = "https://x.com/compose/post" })
end
-- Bindings for preinstalled Omarchy applications, TUIs, and web apps.
o.bind("SUPER + ALT + RETURN", "Tmux", { omarchy = "terminal-tmux" })
o.bind("SUPER + SHIFT + M", "Music", { omarchy = "spotify" })
o.bind("SUPER + SHIFT + ALT + M", "Music TUI", { tui = "cliamp", focus = true })
o.bind("SUPER + SHIFT + D", "Docker", { tui = "lazydocker" })
o.bind("SUPER + SHIFT + G", "Signal", { omarchy = "signal" })
o.bind("SUPER + SHIFT + O", "Obsidian", { launch = "obsidian", focus = "^obsidian$" })
o.bind("SUPER + SHIFT + W", "Omawrite", { launch = "omawrite" })
o.bind("SUPER + SHIFT + SLASH", "Passwords", { omarchy = "1password" })
o.bind("SUPER + SHIFT + A", "ChatGPT", { webapp = "https://chatgpt.com" })
o.bind("SUPER + SHIFT + ALT + A", "Grok", { webapp = "https://grok.com" })
o.bind("SUPER + SHIFT + C", "Calendar", { webapp = "https://app.hey.com/calendar/weeks/" })
o.bind("SUPER + SHIFT + E", "Email", { webapp = "https://app.hey.com" })
o.bind("SUPER + SHIFT + ALT + E", "New email", { webapp = "https://app.hey.com/messages/new?display=standalone&new_window=true" })
o.bind("SUPER + SHIFT + Y", "YouTube", { webapp = "https://youtube.com/" })
o.bind("SUPER + SHIFT + ALT + G", "WhatsApp", { webapp = "https://web.whatsapp.com/", focus = true })
o.bind(
"SUPER + SHIFT + CTRL + G",
"Google Messages",
{ webapp = "https://messages.google.com/web/conversations", focus = true }
)
o.bind("SUPER + SHIFT + P", "Google Photos", { webapp = "https://photos.google.com/", focus = true })
o.bind("SUPER + SHIFT + S", "Google Maps", { webapp = "https://maps.google.com/", focus = true })
o.bind("SUPER + SHIFT + X", "X", { webapp = "https://x.com/" })
o.bind("SUPER + SHIFT + ALT + X", "X Post", { webapp = "https://x.com/compose/post" })
+8 -3
View File
@@ -1,11 +1,16 @@
-- Work around Hyprland send_shortcut sometimes leaving synthetic key state stuck/repeating.
-- Send with explicit mods to the focused surface by omitting the window target,
-- so universal clipboard shortcuts reach both normal windows and focused
-- layer-shell surfaces such as Omarchy panels. A virtual keyboard (wtype) won't
-- do: the physically held SUPER merges into the injected chord at the seat.
-- The down/up split works around Hyprland send_shortcut sometimes leaving
-- synthetic key state stuck/repeating.
-- https://github.com/hyprwm/Hyprland/discussions/14099
local function send_shortcut_once(mods, key)
return function()
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "down", window = "activewindow" }))
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "down" }))
hl.timer(function()
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "up", window = "activewindow" }))
hl.dispatch(hl.dsp.send_key_state({ mods = mods, key = key, state = "up" }))
end, { timeout = 50, type = "oneshot" })
end
end
+3 -7
View File
@@ -1,21 +1,17 @@
o.bind("SUPER + SPACE", "Launch apps", "omarchy-shell shell toggle omarchy.launcher \"{}\"")
o.bind("SUPER + SPACE", "Omarchy menu", "omarchy-menu toggle")
o.bind("SUPER + CTRL + E", "Emojis", "omarchy-shell shell toggle omarchy.emojis")
o.bind("SUPER + CTRL + C", "Capture menu", "omarchy-menu toggle capture")
o.bind("SUPER + CTRL + O", "Toggle menu", "omarchy-menu toggle toggle")
o.bind("SUPER + CTRL + H", "Hardware menu", "omarchy-menu toggle hardware")
o.bind("SUPER + ALT + SPACE", "Omarchy menu", "omarchy-menu toggle root")
o.bind("SUPER + SHIFT + code:201", "Omarchy menu", "omarchy-menu toggle root")
o.bind("SUPER + ESCAPE", "System menu", "omarchy-menu toggle system")
o.bind("XF86PowerOff", "Power menu", "omarchy-menu toggle system", { locked = true })
o.bind("SUPER + K", "Show key bindings", "omarchy-menu-keybindings")
o.bind("SUPER + ALT + K", "Show Tmux key bindings", "omarchy-menu-tmux-keybindings")
o.bind("SUPER + CTRL + J", "Jump to waiting Tmux pane", "omarchy-tmux-alert focus")
o.bind("XF86Calculator", "Calculator", "gnome-calculator")
o.bind_toggle("SUPER + SHIFT + SPACE", "Toggle top bar", "bar")
o.bind("SUPER + SHIFT + CTRL + UP", "Move bar to top", "omarchy-bar position top")
o.bind("SUPER + SHIFT + CTRL + DOWN", "Move bar to bottom", "omarchy-bar position bottom")
o.bind("SUPER + SHIFT + CTRL + LEFT", "Move bar to left", "omarchy-bar position left")
o.bind("SUPER + SHIFT + CTRL + RIGHT", "Move bar to right", "omarchy-bar position right")
o.bind("SUPER + CTRL + SPACE", "Background switcher", "omarchy-menu toggle background")
o.bind("SUPER + SHIFT + CTRL + SPACE", "Theme menu", "omarchy-menu toggle theme")
o.bind("SUPER + BACKSPACE", "Toggle window transparency", "omarchy-hyprland-window-transparency-toggle")
@@ -33,7 +29,7 @@ o.bind_toggle("SUPER + CTRL + I", "Toggle locking on idle", "idle")
o.bind_toggle("SUPER + CTRL + N", "Toggle nightlight", "nightlight")
o.bind("SUPER + CTRL + Delete", "Toggle laptop display", "omarchy-hyprland-monitor-internal toggle")
o.bind("SUPER + CTRL + ALT + Delete", "Toggle laptop display mirroring", "omarchy-hyprland-monitor-internal-mirror toggle")
o.bind("switch:on:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
o.bind("switch:on:Lid Switch", nil, "omarchy-system-lid-close", { locked = true })
o.bind("switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell", { locked = true })
o.bind("PRINT", "Screenshot", "omarchy-capture-screenshot")
+23 -3
View File
@@ -21,14 +21,34 @@ local function read_vconsole()
return values
end
-- Layouts that can't type Latin letters. Keep in sync with the list in
-- etc/mkinitcpio.conf.d/omarchy_hooks.conf.
local non_latin_layouts =
" af am ara bd bg by et ge gr il in iq ir kg kh kz la lk mk mm mn mv np rs ru sy th tj ua "
local vconsole = read_vconsole()
local kb_layout = vconsole.XKBLAYOUT or "us"
local kb_variant = vconsole.XKBVARIANT or ""
local kb_options = "compose:caps,shift:both_capslock"
-- Hyprland resolves keybindings against the first entry in kb_layout, not the
-- layout that's currently active, so Omarchy's Latin-keysym bindings (SUPER + W
-- and friends) only fire when a Latin layout leads. Installing with a non-Latin
-- one would otherwise leave the desktop unusable.
if non_latin_layouts:find(" " .. kb_layout:match("^[^,]*") .. " ", 1, true) then
kb_layout = "us," .. kb_layout
kb_variant = "," .. kb_variant
-- Reach the original layout with Left Alt + Right Alt.
kb_options = kb_options .. ",grp:alts_toggle"
end
hl.config({
input = {
kb_layout = vconsole.XKBLAYOUT or "us",
kb_variant = vconsole.XKBVARIANT or "",
kb_layout = kb_layout,
kb_variant = kb_variant,
kb_model = "",
kb_options = "compose:caps,shift:both_capslock",
kb_options = kb_options,
kb_rules = "",
follow_mouse = 1,
sensitivity = 0,
+2
View File
@@ -5,3 +5,5 @@ local toggles_dir = paths.state_home .. "/omarchy/toggles/hypr"
package.path = toggles_dir .. "/?.lua;" .. package.path
require_all.files(toggles_dir, nil, { reload = true })
require("default.hypr.workspace-layouts")
+8
View File
@@ -0,0 +1,8 @@
-- Restore workspace layouts saved by omarchy-hyprland-workspace-layout-toggle.
local paths = require("default.hypr.paths")
local require_all = require("default.hypr.require_all")
local layouts_dir = paths.state_home .. "/omarchy/workspace-layouts"
require_all.files(layouts_dir, "omarchy.workspace-layouts", { reload = true })
+8 -7
View File
@@ -14,7 +14,7 @@
// checked shell condition; append ✓ when it succeeds
// Root Menu
"apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"action":"omarchy-shell shell summon omarchy.launcher"},
"apps": {"icon":"󰀻","label":"Apps","aliases":["app","applications"],"provider":"apps"},
"learn": {"icon":"󰧑","label":"Learn"},
"trigger": {"icon":"󱓞","label":"Trigger"},
"style": {"icon":"","label":"Style"},
@@ -44,6 +44,7 @@
"learn.tmux-keybindings": {"icon":"","label":"Tmux","action":"omarchy-menu-tmux-keybindings"},
// Trigger
"trigger.emoji": {"icon":"","label":"Emoji","aliases":["emoji","emojis"],"action":"omarchy-menu-emoji"},
"trigger.reminder": {"icon":"󰢌","label":"Reminder","aliases":["reminder"]},
"trigger.capture": {"icon":"","label":"Capture","aliases":["capture","screenshot","screenrecord","screen-record","screenrecording"]},
"trigger.capture.screenshot": {"icon":"","label":"Screenshot","action":"omarchy-capture-screenshot"},
@@ -54,7 +55,7 @@
"trigger.capture.screenrecord.no-audio": {"icon":"","label":"With no audio","action":"omarchy-capture-screenrecording"},
"trigger.capture.screenrecord.desktop-audio": {"icon":"","label":"With desktop audio","action":"omarchy-capture-screenrecording --with-desktop-audio"},
"trigger.capture.screenrecord.microphone": {"icon":"","label":"With desktop + microphone audio","action":"omarchy-capture-screenrecording --with-desktop-audio --with-microphone-audio"},
"trigger.capture.screenrecord.webcam": {"icon":"","label":"With desktop + microphone audio + webcam","action":"omarchy-capture-screenrecording-with-webcam"},
"trigger.capture.screenrecord.webcam": {"icon":"","label":"With desktop + microphone audio + webcam","when":"omarchy-hw-webcam","action":"omarchy-capture-screenrecording-with-webcam"},
"trigger.transcode": {"icon":"󰧸","label":"Transcode","action":"omarchy-transcode"},
"trigger.share": {"icon":"","label":"Share","aliases":["share"]},
"trigger.toggle": {"icon":"󰔎","label":"Toggle","aliases":["toggle","toggles"]},
@@ -120,7 +121,7 @@
"setup.monitors": {"icon":"󰍹","label":"Monitors","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/monitors.lua\""},
"setup.keybindings": {"icon":"","label":"Keybindings","when":"[[ -f ~/.config/hypr/bindings.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/bindings.lua\""},
"setup.input": {"icon":"","label":"Input","when":"[[ -f ~/.config/hypr/input.lua ]]","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/input.lua\""},
"setup.direct-boot": {"icon":"","label":"Direct Boot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-config-direct-boot"},
"setup.direct-boot": {"icon":"","label":"Direct Boot","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-direct-boot"},
"setup.default": {"icon":"","label":"Defaults","aliases":["default","defaults"]},
"setup.default.browser": {"icon":"","label":"Browser"},
"setup.default.browser.chromium": {"icon":"","label":"Chromium","when":"omarchy-cmd-present chromium","checked":"[[ \"$(omarchy-default-browser)\" == \"chromium\" ]]","action":"omarchy-default-browser chromium"},
@@ -128,7 +129,7 @@
"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-origin": {"icon":"󰖟","label":"Brave Origin","when":"omarchy-cmd-present brave-origin","checked":"[[ \"$(omarchy-default-browser)\" == \"brave-origin\" ]]","action":"omarchy-default-browser brave-origin"},
"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.terminal": {"icon":"","label":"Terminal"},
"setup.default.terminal.alacritty": {"icon":"","label":"Alacritty","when":"omarchy-cmd-present alacritty","checked":"[[ \"$(omarchy-default-terminal)\" == \"alacritty\" ]]","action":"omarchy-default-terminal alacritty"},
@@ -146,7 +147,7 @@
"setup.default.editor.emacs": {"icon":"","label":"Emacs","when":"omarchy-cmd-present emacs","checked":"[[ \"$(omarchy-default-editor)\" == \"emacs\" ]]","action":"omarchy-default-editor emacs"},
"setup.security": {"icon":"","label":"Security"},
"setup.config": {"icon":"","label":"Config"},
"setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
"setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","when":"omarchy-hw-fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
"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.passwordless-sudo": {"icon":"󰟵","label":"Passwordless Sudo","action":"omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless"},
@@ -189,14 +190,14 @@
"install.editor.sublime": {"icon":"","label":"Sublime Text","action":"omarchy-install-and-launch 'Sublime Text' sublime-text-4 sublime_text"},
"install.editor.helix": {"icon":"","label":"Helix","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-helix"},
"install.editor.vim": {"icon":"","label":"Vim","action":"omarchy-install-app Vim vim"},
"install.editor.emacs": {"icon":"","label":"Emacs","action":"omarchy-install-app Emacs emacs-wayland && systemctl --user enable --now emacs.service"},
"install.editor.emacs": {"icon":"","label":"Emacs","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-editor-emacs"},
"install.terminal.alacritty": {"icon":"","label":"Alacritty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal alacritty'"},
"install.terminal.foot": {"icon":"","label":"Foot","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal foot'"},
"install.terminal.ghostty": {"icon":"","label":"Ghostty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal ghostty'"},
"install.terminal.kitty": {"icon":"","label":"Kitty","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal kitty'"},
"install.ai.dictation": {"icon":"","label":"Dictation","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
"install.ai.lm-studio": {"icon":"󱚤","label":"LM Studio","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
"install.ai.ollama": {"icon":"󱚤","label":"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; install Ollama \"$ollama_pkg\""},
"install.ai.ollama": {"icon":"󱚤","label":"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.crush": {"icon":"󱚤","label":"Crush","action":"omarchy-install-app Crush crush-bin"},
"install.gaming.steam": {"icon":"","label":"Steam","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-steam"},
"install.gaming.retroarch": {"icon":"󰯉","label":"RetroArch","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-gaming-retroarch"},
@@ -0,0 +1,16 @@
[Unit]
Description=Notify about pending Omarchy migrations
# Login-only. There used to be an omarchy-update-user-notify.path watching
# /usr/share/omarchy/migrations, but pacman writes that directory during every
# update -- including the blessed `omarchy update`, which runs omarchy-migrate
# itself a step later -- so the watcher notified about migrations that were
# already being applied in the visible update terminal. Checking once per login
# is the only trigger that cannot collide with a running update.
ConditionPathIsDirectory=/usr/share/omarchy/migrations
[Service]
Type=oneshot
ExecStart=/usr/bin/omarchy-migrate-notify
[Install]
WantedBy=graphical-session.target
@@ -0,0 +1,32 @@
[Unit]
Description=Omarchy speaker tuning filter-chain
Documentation=https://github.com/basecamp/omarchy/blob/master/docs/AUDIO-TUNING.md
# WirePlumber does the linking, so starting before it is up risks the output being
# linked before the speaker device has been discovered.
After=pipewire.service wireplumber.service
Requires=pipewire.service
Wants=wireplumber.service
# Restart with the audio daemon, since the filter-chain loses its connection when
# PipeWire goes away.
PartOf=pipewire.service
[Service]
Type=simple
# Hosts the tuning as a PipeWire *client* rather than loading it into the daemon
# from pipewire.conf.d, which is only read at daemon startup. That is what lets
# the tuning be switched on and off without restarting pipewire-pulse -- a
# restart drops every PulseAudio client's connection, and applications that do
# not reconnect (Spotify) have to be restarted by hand.
#
# It also contains failure: a malformed tuning breaks only this service, where a
# bad drop-in in the daemon's own config stops PipeWire from starting at all.
#
# The config name is deliberately not PipeWire's stock filter-chain.conf, which
# merges every fragment in ~/.config/pipewire/filter-chain.conf.d/ and would make
# this service host unrelated user filters too.
ExecStart=/usr/bin/pipewire -c omarchy-speaker-tuning.conf
Restart=on-failure
RestartSec=2
[Install]
WantedBy=graphical-session.target
@@ -1,14 +0,0 @@
[Unit]
Description=Watch for Omarchy migrations
[Path]
# Edge-triggered watch only. PathExistsGlob= is level-triggered: it re-fires
# every time the triggered unit deactivates for as long as the glob matches,
# and applied migrations stay on disk forever, so a glob here busy-loops the
# oneshot service. The once-per-login check lives in the service's own
# WantedBy=graphical-session.target instead.
PathModified=/usr/share/omarchy/migrations
Unit=omarchy-update-user-notify.service
[Install]
WantedBy=graphical-session.target

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