Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
242627e
Add omarchy keyboard layout bar plugin
heyssh Jul 23, 2026
40228ae
Add Setup toggle to show/hide the keyboard layout bar icon
heyssh Jul 25, 2026
dc7c924
Merge branch 'quattro' into add/omarchy-keyboard-layout
heyssh Jul 25, 2026
3fd646b
Potential fix for pull request finding
heyssh Jul 25, 2026
8ac3a1e
Update omarchy-keyboard-layout
heyssh Jul 25, 2026
fb89c8f
Update Model.js
heyssh Jul 25, 2026
e8dbee1
Improve panel and model files and add more descriptive comments
heyssh Jul 25, 2026
fa9f789
Remove Both Shift preset, add keybinding to open panel when enabled
heyssh Jul 26, 2026
97d6ddb
fix(panel): Enter in add-language search now adds highlighted row, no…
heyssh Jul 26, 2026
422c956
Rename "Keyboard" label to "Keyboard layout"
heyssh Jul 26, 2026
1dd80e4
Update omarchy-menu.jsonc
heyssh Jul 26, 2026
3a4e2d6
Update keyboard layout binding
heyssh Jul 26, 2026
062ce31
Update "keyboard layout" label to "keyboard layout while enabled"
heyssh Jul 26, 2026
ea0a9ae
Update Panel.qml
heyssh Jul 26, 2026
b3c1d8b
Potential fix for pull request finding
heyssh Jul 26, 2026
01008bf
feat(osd): improve notification display for toggle actions
heyssh Jul 27, 2026
07746fb
default/hypr: sync input.lua with keyboard panel
heyssh Jul 28, 2026
29fe044
fix(keyboard-panel): allow Up from any switcher pill to reach languages
heyssh Jul 28, 2026
2354816
Merge branch 'quattro' into add/omarchy-keyboard-layout
heyssh Jul 28, 2026
92b66d9
fix(keyboard-panel): stop repeated Enter from adding extra languages
heyssh Jul 29, 2026
f7aa9df
improve keyboard panel
heyssh Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions bin/omarchy-keyboard-layout
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#!/bin/bash

# omarchy:summary=Get, switch, or configure keyboard layouts
# omarchy:group=keyboard
# omarchy:args=[status|available|next|set|add|remove|switcher] [value]
# omarchy:examples=omarchy keyboard layout add de

set -e

STATE_DIR="$HOME/.local/state/omarchy/settings"
STATE_FILE="$STATE_DIR/keyboard.json"
XKB_LIST="/usr/share/X11/xkb/rules/base.lst"

usage() {
echo "Usage: omarchy-keyboard-layout [status|available|next|set <code>|add <code>|remove <code>|switcher <preset>]" >&2
exit 1
}

# Looks up the human label xkb ships for a layout code, e.g. "ir" -> "Persian".
# This is the same file base.lst that `localectl list-x11-keymap-layouts` reads.
layout_label() {
local code=$1
awk -v code="$code" '
/^! layout/ { insection=1; next }
/^!/ { insection=0 }
insection && $1 == code {
line = $0
sub(/^[ \t]+/, "", line)
sub(/^[^ \t]+[ \t]+/, "", line)
print line
exit
}
' "$XKB_LIST"
}

# Every layout code + label xkb knows about, as a JSON array, sorted by label.
# This feeds the panel's "Add language" search list.
available_layouts_json() {
awk '
/^! layout/ { insection=1; next }
/^!/ { insection=0 }
insection && NF && $1 != "custom" {
rest = $0
sub(/^[ \t]+/, "", rest)
code = $1
sub(/^[^ \t]+[ \t]+/, "", rest)
printf "%s\t%s\n", code, rest
}
' "$XKB_LIST" | sort -t $'\t' -k2 | jq -R -s '
split("\n") | map(select(length > 0) | split("\t") | {code: .[0], label: .[1]})
'
}

ensure_state() {
mkdir -p "$STATE_DIR"
[[ -f $STATE_FILE ]] && return

local primary label
primary=$(awk -F= '/^XKBLAYOUT/{gsub(/"/, "", $2); print $2}' /etc/vconsole.conf 2>/dev/null || true)
primary=${primary:-us}
label=$(layout_label "$primary")

jq -n --arg code "$primary" --arg label "${label:-$primary}" \
'{layouts: [{code: $code, label: $label}], switcher: "alt_shift"}' > "$STATE_FILE"
}

main_keyboard() {
hyprctl devices -j | jq -r '
[.keyboards[] | select(.name | test("virtual|fcitx|ibus|video-bus|buttons"; "i") | not)] as $real
| (($real[] | select(.main == true) | .name) // ($real[0].name // empty))
'
}

# hyprctl reports the active layout as a human description ("English (US)",
# "Persian"), not the xkb code we store, so match it against the labels we
# already saved for the configured layouts rather than re-deriving a code
# from free text.
active_index() {
local device keymap
device=$(main_keyboard)
[[ -n $device ]] || { echo 0; return; }

keymap=$(hyprctl devices -j | jq -r --arg dev "$device" \
'.keyboards[] | select(.name == $dev) | .active_keymap // ""')

jq -r --arg keymap "$keymap" '
([.layouts[].label] | map(. as $l | (($keymap|ascii_downcase) | contains($l|ascii_downcase)))
| index(true)) // 0
' "$STATE_FILE"
}

status_json() {
local idx
idx=$(active_index)
jq --argjson idx "$idx" '. + {active: (.layouts[$idx].code // .layouts[0].code)}' "$STATE_FILE"
}

reload_hyprland() {
hyprctl reload >/dev/null 2>&1 || true
}

write_state() {
local filter=$1; shift
local tmp
tmp=$(mktemp)
jq "$@" "$filter" "$STATE_FILE" > "$tmp"
mv "$tmp" "$STATE_FILE"
}

action=${1:-status}
ensure_state

case "$action" in
status)
status_json
;;

available)
available_layouts_json
;;

next)
device=$(main_keyboard)
[[ -n $device ]] || { echo "No keyboard device found" >&2; exit 1; }
hyprctl switchxkblayout "$device" next >/dev/null
status_json
;;

set)
code=${2:-}
[[ -n $code ]] || usage
device=$(main_keyboard)
[[ -n $device ]] || { echo "No keyboard device found" >&2; exit 1; }
index=$(jq -r --arg code "$code" '[.layouts[].code] | index($code) // empty' "$STATE_FILE")
[[ -n $index ]] || { echo "Layout '$code' is not configured" >&2; exit 1; }
hyprctl switchxkblayout "$device" "$index" >/dev/null
status_json
;;

add)
code=${2:-}
[[ -n $code ]] || usage
already=$(jq -r --arg code "$code" '[.layouts[].code] | index($code) // empty' "$STATE_FILE")
if [[ -z $already ]]; then
label=$(layout_label "$code")
[[ -n $label ]] || { echo "Unknown layout '$code'" >&2; exit 1; }
write_state '.layouts += [{code: $code, label: $label}]' --arg code "$code" --arg label "$label"
reload_hyprland
fi
status_json
;;

remove)
code=${2:-}
[[ -n $code ]] || usage
count=$(jq '.layouts | length' "$STATE_FILE")
primary_code=$(jq -r '.layouts[0].code // empty' "$STATE_FILE")
if [[ $count -gt 1 && $code != "$primary_code" ]]; then
write_state '.layouts |= map(select(.code != $code))' --arg code "$code"
reload_hyprland
fi
status_json
;;

switcher)
preset=${2:-}
case "$preset" in
alt_shift|ctrl_shift|right_alt|both_shift) ;;
*) usage ;;
esac
write_state '.switcher = $s' --arg s "$preset"
reload_hyprland
status_json
;;

*)
usage
;;
esac
160 changes: 103 additions & 57 deletions config/hypr/input.lua
Original file line number Diff line number Diff line change
@@ -1,57 +1,103 @@
-- Keep only your personal input overrides here. Uncommented settings below
-- replace Omarchy's defaults.

-- Keyboard layout and options.
-- See https://wiki.hypr.land/Configuring/Basics/Variables/#input
-- hl.config({
-- input = {
-- -- Use multiple keyboard layouts and switch between them with Left Alt + Right Alt.
-- kb_layout = "us,dk,eu",
-- kb_options = "compose:caps,shift:both_capslock,grp:alts_toggle",
--
-- -- Use a specific keyboard variant if needed (e.g. intl for international keyboards).
-- kb_variant = "intl",
--
-- -- Change speed of keyboard repeat.
-- repeat_rate = 40,
-- repeat_delay = 250,
--
-- -- Start with numlock on by default.
-- numlock_by_default = true,
--
-- -- Increase sensitivity for mouse/trackpad (default: 0).
-- sensitivity = 0.35,
--
-- -- Turn off mouse acceleration (default: adaptive).
-- accel_profile = "flat",
--
-- touchpad = {
-- -- Use natural (inverse) scrolling.
-- natural_scroll = true,
--
-- -- Use two-finger clicks for right-click instead of lower-right corner.
-- clickfinger_behavior = true,
--
-- -- Control the speed of your scrolling.
-- scroll_factor = 0.4,
--
-- -- Enable the touchpad while typing.
-- disable_while_typing = false,
--
-- -- Left-click-and-drag with three fingers.
-- drag_3fg = 1,
-- },
-- },
-- })

-- App-specific touchpad scroll speeds.
-- o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
-- o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 })

-- Enable touchpad gestures for changing workspaces.
-- See https://wiki.hypr.land/Configuring/Advanced-and-Cool/Gestures/
-- hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })

-- Enable touchpad gestures for moving focus (helpful on scrolling layout).
-- hl.gesture({ fingers = 3, direction = "left", action = function() hl.dispatch(hl.dsp.focus({ direction = "l" })) end })
-- hl.gesture({ fingers = 3, direction = "right", action = function() hl.dispatch(hl.dsp.focus({ direction = "r" })) end })
-- https://wiki.hypr.land/Configuring/Basics/Variables/#input

local function read_vconsole()
local values = {}
local file = io.open("/etc/vconsole.conf", "r")
if not file then
return values
end

for line in file:lines() do
local key, value = line:match("^%s*([%w_]+)%s*=%s*(.-)%s*$")
if key and value then
value = value:gsub("%s+#.*$", "")
value = value:gsub('^"(.*)"$', "%1")
value = value:gsub("^'(.*)'$", "%1")
values[key] = value
end
end

file:close()
return values
end

-- The keyboard panel (bin/omarchy-keyboard-layout) owns this file: it's the
-- list of layouts the user picked via "Add language" plus their chosen
-- switch shortcut. jq is already a hard dependency of the Omarchy CLI
-- scripts, so shelling out to it here is simpler and less error-prone than
-- hand-rolling JSON parsing in Lua.
Comment thread
heyssh marked this conversation as resolved.
Outdated
local STATE_FILE = os.getenv("HOME") .. "/.local/state/omarchy/settings/keyboard.json"

local function popen_trim(cmd)
local handle = io.popen(cmd)
if not handle then
return ""
end
local result = handle:read("*a") or ""
handle:close()
return (result:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function state_field(jq_filter, fallback)
local cmd = string.format("jq -r %s %q 2>/dev/null", jq_filter, STATE_FILE)
local value = popen_trim(cmd)
if value == "" or value == "null" then
return fallback
end
return value
end

local vconsole = read_vconsole()

local kb_layout = state_field("'[.layouts[].code] | join(\",\")'", vconsole.XKBLAYOUT or "us")
local switcher = state_field("'.switcher // \"alt_shift\"'", "alt_shift")

-- The four switch-shortcut presets offered by the keyboard panel's pill
-- row, mapped to the matching xkb grp option.
local SWITCHER_OPTIONS = {
alt_shift = "grp:alt_shift_toggle",
ctrl_shift = "grp:ctrl_shift_toggle",
right_alt = "grp:toggle",
both_shift = "grp:shifts_toggle",
}

local grp_option = SWITCHER_OPTIONS[switcher] or SWITCHER_OPTIONS.alt_shift

-- Compose used to be bound to "compose:caps", which hijacks Caps Lock
-- entirely and turns it into the Compose key -- Caps Lock then never
-- toggles capitalization again, it just shows the small compose-pending
-- dot while waiting for the next key. Bind Compose to the Menu key
-- instead: it's unused by every switcher preset above, so Caps Lock keeps
-- working normally and Compose sequences (e.g. Menu, ', e -> é) still work.
local kb_options = "compose:menu," .. grp_option

hl.config({
input = {
kb_layout = kb_layout,
kb_variant = "",
kb_model = "",
kb_options = kb_options,
kb_rules = "",
follow_mouse = 1,
sensitivity = 0,

repeat_rate = 40,
repeat_delay = 250,
numlock_by_default = true,

touchpad = {
natural_scroll = false,
clickfinger_behavior = true,
scroll_factor = 0.4,
},
},

misc = {
key_press_enables_dpms = true,
mouse_move_enables_dpms = true,
},
})

-- Scroll nicely in the terminal.
o.window("(Alacritty|kitty|foot)", { scroll_touchpad = 1.5 })
o.window("com.mitchellh.ghostty", { scroll_touchpad = 0.2 })
Loading