diff --git a/bin/omarchy-hyprland-corners b/bin/omarchy-hyprland-corners new file mode 100755 index 00000000..0d73ed56 --- /dev/null +++ b/bin/omarchy-hyprland-corners @@ -0,0 +1,41 @@ +#!/bin/bash + +# omarchy:summary=Show or set window corners: theme, sharp, or rounded +# omarchy:args=[theme|sharp|rounded] +# omarchy:examples=omarchy hyprland corners | omarchy hyprland corners rounded + +TOGGLES_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/omarchy/toggles/hypr" + +# Sharp and rounded are toggle files loaded after every theme, so they override +# the corners of whichever theme is current. Theme removes both. +if (( $# == 0 )); then + if [[ -f $TOGGLES_DIR/corners-sharp.lua ]]; then + echo "sharp" + elif [[ -f $TOGGLES_DIR/corners-rounded.lua ]]; then + echo "rounded" + else + echo "theme" + fi + exit 0 +fi + +case $1 in + theme) LABEL="Corners follow the theme" ;; + sharp) LABEL="Sharp corners for all themes" ;; + rounded) LABEL="Rounded corners for all themes" ;; + *) + echo "Usage: omarchy-hyprland-corners [theme|sharp|rounded]" >&2 + exit 1 + ;; +esac + +# Save the new choice before dropping the old one, so a failed save keeps it. +if [[ $1 != "theme" ]]; then + mkdir -p "$TOGGLES_DIR" && cp "$OMARCHY_PATH/default/hypr/toggles/corners-$1.lua" "$TOGGLES_DIR/corners-$1.lua" || exit 1 +fi +for mode in sharp rounded; do + [[ $mode == "$1" ]] || rm -f "$TOGGLES_DIR/corners-$mode.lua" +done + +hyprctl reload >/dev/null || exit 1 +omarchy-notification-send -g 󰘇 "$LABEL" diff --git a/default/hypr/toggles/corners-rounded.lua b/default/hypr/toggles/corners-rounded.lua new file mode 100644 index 00000000..06b54e18 --- /dev/null +++ b/default/hypr/toggles/corners-rounded.lua @@ -0,0 +1,7 @@ +-- Round window corners, whatever the theme sets. Matches Solitude's corners. +hl.config({ + decoration = { + rounding = 6, + rounding_power = 3, + }, +}) diff --git a/default/hypr/toggles/corners-sharp.lua b/default/hypr/toggles/corners-sharp.lua new file mode 100644 index 00000000..df2fe1c7 --- /dev/null +++ b/default/hypr/toggles/corners-sharp.lua @@ -0,0 +1,6 @@ +-- Square window corners, whatever the theme sets. +hl.config({ + decoration = { + rounding = 0, + }, +}) diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index d0cbfb89..364c99c5 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -83,6 +83,10 @@ "trigger.appearance.top-bar.position.left": {"icon":"󰁍","label":"Left Bar","action":"omarchy-bar position left"}, "trigger.appearance.top-bar.position.right": {"icon":"󰁔","label":"Right Bar","action":"omarchy-bar position right"}, "trigger.appearance.window-gaps": {"icon":"","label":"Window Gaps","action":"omarchy-hyprland-window-gaps-toggle"}, + "trigger.appearance.window-corners": {"icon":"󰘇","label":"Window Corners"}, + "trigger.appearance.window-corners.theme": {"icon":"󰏘","label":"Theme Corners","checked":"[[ $(omarchy-hyprland-corners) == \"theme\" ]]","action":"omarchy-hyprland-corners theme"}, + "trigger.appearance.window-corners.sharp": {"icon":"󰝣","label":"Sharp Corners","checked":"[[ $(omarchy-hyprland-corners) == \"sharp\" ]]","action":"omarchy-hyprland-corners sharp"}, + "trigger.appearance.window-corners.rounded": {"icon":"󰘇","label":"Rounded Corners","checked":"[[ $(omarchy-hyprland-corners) == \"rounded\" ]]","action":"omarchy-hyprland-corners rounded"}, "trigger.appearance.one-window-ratio": {"icon":"","label":"1-Window Ratio","action":"omarchy-hyprland-window-single-square-aspect-toggle"}, "trigger.appearance.workspace-layout": {"icon":"󱂬","label":"Workspace Layout","title":"Default Workspace Layout"}, "trigger.appearance.workspace-layout.dwindle": {"icon":"󱓻","label":"Tiling","checked":"[[ $(omarchy-hyprland-workspace-layout-default) == \"dwindle\" ]]","action":"omarchy-hyprland-workspace-layout-default dwindle"}, diff --git a/shell/plugins/menu/MenuModel.js b/shell/plugins/menu/MenuModel.js index 74507c85..f2dbbeb7 100644 --- a/shell/plugins/menu/MenuModel.js +++ b/shell/plugins/menu/MenuModel.js @@ -402,7 +402,9 @@ var GUARD_READERS = [ "omarchy-default-editor", "omarchy-default-terminal", "omarchy-default-dictation", - "omarchy-dns" + "omarchy-dns", + "omarchy-hyprland-corners", + "omarchy-hyprland-workspace-layout-default" ] // Package and command presence account for most of what the guards ask, and diff --git a/test/shell.d/hyprland-corners-test.sh b/test/shell.d/hyprland-corners-test.sh new file mode 100755 index 00000000..6652c83b --- /dev/null +++ b/test/shell.d/hyprland-corners-test.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh" + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +stub_dir="$tmpdir/bin" +home_dir="$tmpdir/home" +toggles_dir="$home_dir/.local/state/omarchy/toggles/hypr" +mkdir -p "$stub_dir" "$home_dir" + +for command in hyprctl omarchy-notification-send; do + printf '#!/bin/bash\nprintf "%%s\\n" "%s $*" >>"%s/commands.log"\n' "$command" "$tmpdir" >"$stub_dir/$command" +done +chmod +x "$stub_dir"/* + +run() { + HOME="$home_dir" XDG_STATE_HOME="$home_dir/.local/state" OMARCHY_PATH="$ROOT" PATH="$stub_dir:$ROOT/bin:$PATH" "$@" +} + +[[ $(run omarchy-hyprland-corners) == "theme" ]] || fail "corners start out following the theme" +pass "corners start out following the theme" + +run omarchy-hyprland-corners rounded +[[ $(run omarchy-hyprland-corners) == "rounded" ]] || fail "rounded corners are reported" +cmp -s "$ROOT/default/hypr/toggles/corners-rounded.lua" "$toggles_dir/corners-rounded.lua" || fail "rounded corners are saved as a toggle" +grep -qx 'hyprctl reload' "$tmpdir/commands.log" || fail "new corners reload Hyprland" +pass "rounded corners override the theme" + +run omarchy-hyprland-corners sharp +[[ $(run omarchy-hyprland-corners) == "sharp" ]] || fail "sharp corners are reported" +[[ ! -e $toggles_dir/corners-rounded.lua ]] || fail "sharp corners replace rounded ones" +pass "sharp corners replace rounded ones" + +run omarchy-hyprland-corners theme +[[ $(run omarchy-hyprland-corners) == "theme" ]] || fail "theme corners are reported" +[[ ! -e $toggles_dir/corners-sharp.lua ]] || fail "theme corners drop the override" +pass "theme corners drop the override" + +if run omarchy-hyprland-corners square 2>/dev/null; then + fail "unknown corners are refused" +fi +pass "unknown corners are refused" + +# Hyprland loads toggles from XDG_STATE_HOME, so the corners have to land there. +state_home="$tmpdir/state" +HOME="$home_dir" XDG_STATE_HOME="$state_home" OMARCHY_PATH="$ROOT" PATH="$stub_dir:$ROOT/bin:$PATH" omarchy-hyprland-corners sharp +[[ -f $state_home/omarchy/toggles/hypr/corners-sharp.lua ]] || fail "corners follow XDG_STATE_HOME" +[[ $(HOME="$home_dir" XDG_STATE_HOME="$state_home" PATH="$ROOT/bin:$PATH" omarchy-hyprland-corners) == "sharp" ]] || fail "corners are read from XDG_STATE_HOME" +pass "corners follow XDG_STATE_HOME" + +# Rounded corners are Solitude's, so the two must not drift apart. +[[ $(grep -E '^\s*rounding(_power)? =' "$ROOT/default/hypr/toggles/corners-rounded.lua") == "$(grep -E '^\s*rounding(_power)? =' "$ROOT/themes/solitude/hyprland.lua")" ]] || + fail "rounded corners match Solitude's" +pass "rounded corners match Solitude's" + +# A failed save keeps the corners already chosen. +run omarchy-hyprland-corners rounded +chmod a-w "$toggles_dir" +if run omarchy-hyprland-corners sharp 2>/dev/null; then + chmod u+w "$toggles_dir" + fail "a failed save is reported" +fi +chmod u+w "$toggles_dir" +[[ $(run omarchy-hyprland-corners) == "rounded" ]] || fail "a failed save keeps the corners already chosen" +pass "a failed save keeps the corners already chosen" +run omarchy-hyprland-corners theme + +# Hyprland loads the theme, then every toggle in name order: the corners win +# over the theme, and no-gaps still squares them. +if command -v lua >/dev/null; then + toggle_load() { + local dir="$tmpdir/load" + rm -rf "$dir" + mkdir -p "$dir" + for toggle in "$@"; do cp "$ROOT/default/hypr/toggles/$toggle.lua" "$dir/"; done + ROOT="$ROOT" DIR="$dir" lua - <<'LUA' +package.path = os.getenv("ROOT") .. "/?.lua;" .. os.getenv("DIR") .. "/?.lua;" .. package.path +local rounding +hl = { config = function(config) if config.decoration and config.decoration.rounding then rounding = config.decoration.rounding end end } +hl.config({ decoration = { rounding = 10 } }) -- the theme +require("default.hypr.require_all").files(os.getenv("DIR"), nil, { reload = true }) +print(rounding) +LUA + } + [[ $(toggle_load) == "10" ]] || fail "theme corners keep the theme's rounding" + [[ $(toggle_load corners-rounded) == "6" ]] || fail "rounded corners override the theme's rounding" + [[ $(toggle_load corners-sharp) == "0" ]] || fail "sharp corners override the theme's rounding" + [[ $(toggle_load corners-rounded window-no-gaps) == "0" ]] || fail "no-gaps squares rounded corners" + pass "corners load after the theme, and no-gaps still squares them" +else + skip "lua is not installed" +fi diff --git a/test/shell.d/menu-test.sh b/test/shell.d/menu-test.sh index 1630c930..55488054 100644 --- a/test/shell.d/menu-test.sh +++ b/test/shell.d/menu-test.sh @@ -194,8 +194,8 @@ assert( const triggerItems = defaultItems.filter(item => item.parent === 'trigger') assertEqual( triggerItems[0].id, - 'trigger.toggle', - 'menu lists Toggle first under Trigger' + 'trigger.appearance', + 'menu lists Appearance first under Trigger' ) assertEqual( defaultById['trigger.emoji'].action, @@ -412,9 +412,14 @@ assertDeepEqual( ['Top Bar', 'Bottom Bar', 'Left Bar', 'Right Bar'], 'menu names each position after the bar' ) +assertDeepEqual( + defaultItems.filter(item => item.parent === 'trigger.appearance.window-corners').map(item => [item.label, item.action]), + [['Theme Corners', 'omarchy-hyprland-corners theme'], ['Sharp Corners', 'omarchy-hyprland-corners sharp'], ['Rounded Corners', 'omarchy-hyprland-corners rounded']], + 'menu picks window corners from Appearance > Window Corners' +) const triggerIds = triggerItems.map(item => item.id) -const appearanceLabels = ['Animations', 'Menu Bar', 'Window Gaps', '1-Window Ratio', 'Workspace Layout', 'Herdr Theme Sync'] -assertEqual(triggerIds.indexOf('trigger.appearance'), triggerIds.indexOf('trigger.toggle') + 1, 'menu places Appearance right after Toggle') +const appearanceLabels = ['Animations', 'Menu Bar', 'Window Gaps', 'Window Corners', '1-Window Ratio', 'Workspace Layout', 'Herdr Theme Sync'] +assertEqual(triggerIds.indexOf('trigger.toggle'), triggerIds.indexOf('trigger.appearance') + 1, 'menu places Toggle right after Appearance') assertDeepEqual( defaultItems.filter(item => item.parent === 'trigger.appearance').map(item => item.label), appearanceLabels,