Merge remote-tracking branch 'origin/quattro' into fix-snapshot-locate-and-timeline-leak
# Conflicts: # docs/file-layout.md
This commit is contained in:
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+55
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Executable
+357
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:-}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+43
@@ -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
|
||||
Executable
+11
@@ -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
|
||||
Executable
+5
@@ -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'
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Executable
+5
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Executable
+9
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}]'
|
||||
|
||||
Executable
+30
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+62
@@ -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
|
||||
}
|
||||
Executable
+20
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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:-}"
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+18
@@ -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
|
||||
@@ -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'
|
||||
|
||||
Executable
+140
@@ -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
|
||||
Executable
+50
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Executable
+21
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
{}
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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" })
|
||||
@@ -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" })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 })
|
||||
@@ -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
Reference in New Issue
Block a user