Apply typing selections immediately from the menu

This commit is contained in:
David Heinemeier Hansson committed 2026-10-09 16:37:52 +02:00
1 parent 58d7429f53
commit 9421e961ea
9 files changed
+126 -39

No files matched your search

+14 -2
View File
@@ -13,6 +13,8 @@
# with same-named rows get the subtext back as the stable key. # with same-named rows get the subtext back as the stable key.
# With --multiple, clicking/Enter toggles rows and Apply returns a JSON array. # With --multiple, clicking/Enter toggles rows and Apply returns a JSON array.
# --on-change takes a JSON command array; each toggle runs it with the selection
# JSON appended, applies immediately, and needs no Apply row.
# --selected takes a returned value (label plus any subtext), and can be repeated. # --selected takes a returned value (label plus any subtext), and can be repeated.
set -euo pipefail set -euo pipefail
@@ -29,6 +31,7 @@ options=()
menu_width="" menu_width=""
menu_maxheight="" menu_maxheight=""
multiple=0 multiple=0
on_change="[]"
selected=() selected=()
while (( $# > 0 )); do while (( $# > 0 )); do
@@ -36,6 +39,14 @@ while (( $# > 0 )); do
shift shift
while (( $# > 0 )); do while (( $# > 0 )); do
case "$1" in case "$1" in
--on-change)
shift
if (( $# == 0 )); then
echo "omarchy-menu-select: --on-change requires a JSON command array" >&2
exit 1
fi
on_change="$1"
;;
--multiple) --multiple)
multiple=1 multiple=1
;; ;;
@@ -97,15 +108,16 @@ payload=$(perl -MEncode=decode -MJSON::PP=encode_json,decode_json -e '
options => decode_json($ARGV[1]), options => decode_json($ARGV[1]),
multiple => $ARGV[6] ? JSON::PP::true : JSON::PP::false, multiple => $ARGV[6] ? JSON::PP::true : JSON::PP::false,
selected => decode_json($ARGV[7]), selected => decode_json($ARGV[7]),
onChange => decode_json($ARGV[8]),
selectionFile => decode("UTF-8", $ARGV[2]), selectionFile => decode("UTF-8", $ARGV[2]),
doneFile => decode("UTF-8", $ARGV[3]) doneFile => decode("UTF-8", $ARGV[3])
}; };
$payload->{width} = int($ARGV[4]) if length($ARGV[4] // ""); $payload->{width} = int($ARGV[4]) if length($ARGV[4] // "");
$payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // ""); $payload->{maxHeight} = int($ARGV[5]) if length($ARGV[5] // "");
print encode_json($payload) print encode_json($payload)
' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight" "$multiple" "$selected_json") ' "$prompt" "$options_json" "$selection_file" "$done_file" "$menu_width" "$menu_maxheight" "$multiple" "$selected_json" "$on_change")
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null omarchy-shell shell summon omarchy.menu "$payload" >/dev/null || exit 2
while [[ ! -e $done_file ]]; do while [[ ! -e $done_file ]]; do
sleep 0.05 sleep 0.05
+11
View File
@@ -28,6 +28,17 @@ if non_latin_layouts:find(" " .. kb_layout:match("^[^,]*") .. " ", 1, true) then
kb_options = kb_options .. ",grp:alts_toggle" kb_options = kb_options .. ",grp:alts_toggle"
end end
-- Saved selections already include their Latin-leading layout. Keep the
-- installer's layout-switching shortcut when a non-Latin layout remains.
if not kb_options:find("grp:alts_toggle", 1, true) then
for layout in kb_layout:gmatch("[^,]+") do
if non_latin_layouts:find(" " .. layout .. " ", 1, true) then
kb_options = kb_options .. ",grp:alts_toggle"
break
end
end
end
hl.config({ hl.config({
input = { input = {
kb_layout = kb_layout, kb_layout = kb_layout,
+47 -30
View File
@@ -30,14 +30,21 @@ def keyboard_selection():
for index, layout in enumerate(layouts)] for index, layout in enumerate(layouts)]
def choose(title, catalog, selected): def picker_rows(catalog, selected):
# Current entries stay first, in switching order, followed by the catalog. # Current entries stay first, in switching order, followed by the catalog.
keys = list(dict.fromkeys(selected + sorted(catalog, key=lambda key: catalog[key]))) keys = list(dict.fromkeys(selected + sorted(catalog, key=lambda key: catalog[key])))
labels = {key: catalog.get(key, key) for key in keys} labels = {key: catalog.get(key, key) for key in keys}
counts = Counter(labels.values()) counts = Counter(labels.values())
rows = {key: "\t" + labels[key] + (f" ({key})" if counts[labels[key]] > 1 else "") for key in keys} rows = {key: "\t" + labels[key] + (f" ({key})" if counts[labels[key]] > 1 else "") for key in keys}
return rows
def choose(title, catalog, selected, kind):
catalog = {**catalog, **{key: catalog.get(key, key) for key in selected}}
rows = picker_rows(catalog, selected)
args = ["omarchy-menu-select", title] + list(rows.values()) args = ["omarchy-menu-select", title] + list(rows.values())
args += ["--", "--multiple", "--width", "620", "--maxheight", "650"] on_change = ["python", str(setup.ROOT / "default/input-methods/typing.py"), "apply", kind, json.dumps(catalog)]
args += ["--", "--multiple", "--width", "620", "--maxheight", "650", "--on-change", json.dumps(on_change)]
for key in selected: for key in selected:
args += ["--selected", rows[key][1:]] args += ["--selected", rows[key][1:]]
result = subprocess.run(args, text=True, capture_output=True) result = subprocess.run(args, text=True, capture_output=True)
@@ -87,21 +94,20 @@ def configure_inputs():
group, layout, items = setup.live_group() group, layout, items = setup.live_group()
catalog, available = input_catalog(items) catalog, available = input_catalog(items)
current = [name for name, override in items if not name.startswith("keyboard-")] current = [name for name, override in items if not name.startswith("keyboard-")]
selected = choose("Input Methods", catalog, current) choose("Input Methods", catalog, current, "input")
if selected is None:
return False
missing = [catalog[name] for name in selected if name not in available] def save_inputs(selected):
group, layout, items = setup.live_group()
catalog, available = input_catalog(items)
missing = [catalog.get(name, name) for name in selected if name not in available]
if missing: if missing:
raise RuntimeError("Restart input after updating Omarchy to use: " + ", ".join(missing)) raise RuntimeError("Restart input after updating Omarchy to use: " + ", ".join(missing))
# Abort rather than overwrite a group changed while the picker was open.
if setup.live_group() != (group, layout, items):
raise RuntimeError("Input settings changed while the menu was open. Please try again.")
config_home = Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")) config_home = Path(os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config"))
for name in selected: for name in selected:
if name in setup.PRESETS: if name in setup.PRESETS:
setup.font_default(config_home, name) setup.font_default(config_home, name)
set_inputs(group, layout, items, input_items(items, selected)) set_inputs(group, layout, items, input_items(items, selected))
return True
def keyboard_values(selected): def keyboard_values(selected):
@@ -110,7 +116,7 @@ def keyboard_values(selected):
parts = [value.split(":", 1) for value in selected] parts = [value.split(":", 1) for value in selected]
# Preserve the installer's Latin-leading rule for desktop keybindings. # Preserve the installer's Latin-leading rule for desktop keybindings.
if parts[0][0] in setup.NON_LATIN: if parts[0][0] in setup.NON_LATIN:
parts.insert(0, ["us"]) parts = [["us"]] + [part for part in parts if part != ["us"]]
layouts = ",".join(part[0] for part in parts) layouts = ",".join(part[0] for part in parts)
variants = ",".join(part[1] if len(part) > 1 else "" for part in parts) variants = ",".join(part[1] if len(part) > 1 else "" for part in parts)
return layouts, variants return layouts, variants
@@ -123,16 +129,15 @@ def save_keyboard(selected):
original = setup.read(path) if path.exists() else None original = setup.read(path) if path.exists() else None
group, old_layout, before = setup.live_group() group, old_layout, before = setup.live_group()
group_changed = False group_changed = False
previous_errors = setup.run(["hyprctl", "configerrors"])
try: try:
setup.atomic_write(path, f"XKBLAYOUT={layouts}\nXKBVARIANT={variants}\n") setup.atomic_write(path, f"XKBLAYOUT={layouts}\nXKBVARIANT={variants}\n")
setup.run(["hyprctl", "reload"]) setup.run(["hyprctl", "reload"])
errors = setup.run(["hyprctl", "configerrors"]) errors = setup.run(["hyprctl", "configerrors"])
if errors: if errors and errors != previous_errors:
raise RuntimeError(errors) raise RuntimeError(errors)
expected = [part[0] + (":" + part[1] if len(part) > 1 and part[1] else "") expected = [layout + (":" + variant if variant else "")
for part in [value.split(":", 1) for value in selected]] for layout, variant in zip(layouts.split(","), variants.split(","))]
if selected[0].split(":", 1)[0] in setup.NON_LATIN:
expected.insert(0, "us")
if keyboard_selection() != expected: if keyboard_selection() != expected:
raise RuntimeError("Your personal keyboard layout override takes precedence over this selection") raise RuntimeError("Your personal keyboard layout override takes precedence over this selection")
first_layout = layouts.split(",")[0] first_layout = layouts.split(",")[0]
@@ -152,36 +157,48 @@ def save_keyboard(selected):
path.resolve().unlink(missing_ok=True) path.resolve().unlink(missing_ok=True)
else: else:
setup.atomic_write(path, original) setup.atomic_write(path, original)
setup.run(["hyprctl", "reload"]) try:
if group_changed: setup.run(["hyprctl", "reload"])
setup.live_set(group, old_layout, before) finally:
if group_changed:
setup.live_set(group, old_layout, before)
raise raise
def configure_keyboard(): def configure_keyboard():
current = keyboard_selection() current = keyboard_selection()
catalog = keyboard_catalog() catalog = keyboard_catalog()
selected = choose("Keyboard Layouts", catalog, current) choose("Keyboard Layouts", catalog, current, "keyboard")
if selected is None:
return False
if keyboard_selection() != current: def apply_selection(kind, catalog, values):
raise RuntimeError("Keyboard settings changed while the menu was open. Please try again.") rows = picker_rows(catalog, [])
save_keyboard(selected) valid = {row[1:]: key for key, row in rows.items()}
return True if not isinstance(values, list) or any(not isinstance(value, str) or value not in valid for value in values):
raise ValueError("The menu returned an invalid selection")
selected = list(dict.fromkeys(valid[value] for value in values))
if kind == "input":
save_inputs(selected)
else:
save_keyboard(selected)
def main(): def main():
kind = sys.argv[1] if len(sys.argv) == 2 else "" applying = len(sys.argv) == 5 and sys.argv[1] == "apply"
kind = sys.argv[2] if applying else sys.argv[1] if len(sys.argv) == 2 else ""
if kind not in ("input", "keyboard"): if kind not in ("input", "keyboard"):
raise ValueError("Choose keyboard or input setup") raise ValueError("Choose keyboard or input setup")
title = "Input methods" if kind == "input" else "Keyboard layouts" title = "Input methods" if kind == "input" else "Keyboard layouts"
try: try:
applied = configure_inputs() if kind == "input" else configure_keyboard() if applying:
apply_selection(kind, json.loads(sys.argv[3]), json.loads(sys.argv[4]))
elif kind == "input":
configure_inputs()
else:
configure_keyboard()
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error: except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
subprocess.run(["omarchy-notification-send", title + " could not be updated", str(error)], check=True) subprocess.run(["omarchy-notification-send", title + " could not be updated", str(error)], check=True)
raise SystemExit(1) raise SystemExit(1)
if applied:
subprocess.run(["omarchy-notification-send", title + " updated", "Use Super + I to switch inputs"], check=True)
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -10,7 +10,7 @@ The ISO carries the extra fields in `omarchy_install.input_method` and `omarchy_
`omarchy setup input <engine>` installs the selected engine, restarts the supervised Fcitx unit so it discovers new addons, and appends the engine through `InputMethodGroupInfo`, `SetInputMethodGroupInfo`, and `Save`. It keeps the group name, layout, existing methods, and their layout overrides. There is no parallel unsupervised daemon. A per-user D-Bus activation file delegates `org.fcitx.Fcitx5` to `omarchy-fcitx5.service`; it is loaded before live restart. `omarchy setup input <engine>` installs the selected engine, restarts the supervised Fcitx unit so it discovers new addons, and appends the engine through `InputMethodGroupInfo`, `SetInputMethodGroupInfo`, and `Save`. It keeps the group name, layout, existing methods, and their layout overrides. There is no parallel unsupervised daemon. A per-user D-Bus activation file delegates `org.fcitx.Fcitx5` to `omarchy-fcitx5.service`; it is loaded before live restart.
`Setup > Typing > Keyboard Layouts` and `Input Methods` use the native menu's multi-select mode, with current entries checked and an explicit Apply row. `default/input-methods/typing.py` reads the Fcitx keyboard catalog (including variants) and the current input group. Input selection replaces only the non-keyboard group items and retains selected engines' layout overrides; deselection does not uninstall packages. The standard composition engines are already shipped in the base package set. Cancellation does not change configuration. `Setup > Typing > Keyboard Layouts` and `Input Methods` use the native menu's multi-select mode, with current entries checked and changes applied immediately. `default/input-methods/typing.py` reads the Fcitx keyboard catalog (including variants) and the current input group. Input selection replaces only the non-keyboard group items and retains selected engines' layout overrides; deselection does not uninstall packages. The standard composition engines are already shipped in the base package set. Closing a picker keeps changes already applied. Failed updates leave its checked state unchanged.
Keyboard selections are persisted as data in `~/.config/omarchy/keyboard-layouts`; `default.hypr.keyboard.selected()` reads them before falling back to installation defaults. Personal Hyprland overrides still take precedence. A save reloads and validates the compositor, aligns the Fcitx group's primary keyboard with the leading layout/variant, and restores the previous configuration and group on failure. Non-Latin selections keep the installer's Latin-leading rule for desktop shortcuts. System console, disk-unlock, and greeter layouts are unchanged. Keyboard selections are persisted as data in `~/.config/omarchy/keyboard-layouts`; `default.hypr.keyboard.selected()` reads them before falling back to installation defaults. Personal Hyprland overrides still take precedence. A save reloads and validates the compositor, aligns the Fcitx group's primary keyboard with the leading layout/variant, and restores the previous configuration and group on failure. Non-Latin selections keep the installer's Latin-leading rule for desktop shortcuts. System console, disk-unlock, and greeter layouts are unchanged.
+2
View File
@@ -168,3 +168,5 @@ how the pickers behind menu actions (`omarchy-menu-plugin`,
Pass `-- --multiple` to `omarchy-menu-select` for a multi-select request. Repeat `--selected <value>` to preselect returned values (labels plus any subtext, without icons). Clicking or pressing Enter toggles an option without closing the menu; Apply returns a JSON array in option order, including `[]` when all choices are deselected. Cancellation still exits 1. Selections survive filtering. Pass `-- --multiple` to `omarchy-menu-select` for a multi-select request. Repeat `--selected <value>` to preselect returned values (labels plus any subtext, without icons). Clicking or pressing Enter toggles an option without closing the menu; Apply returns a JSON array in option order, including `[]` when all choices are deselected. Cancellation still exits 1. Selections survive filtering.
With `--on-change <JSON command array>`, each toggle runs that command with the proposed selection JSON appended as its final argument, and no Apply row is shown. The checkbox changes only after the command succeeds; a failed command retains the previous selection. Changes persist when the menu closes. The typing selectors use this mode.
+1 -1
View File
@@ -65,7 +65,7 @@ Typing starts in Latin mode. Press `Super + I` to cycle through configured input
The keyboard indicator appears on the right of the bar when you have multiple keyboard layouts or input methods. Click it to switch input languages; Japanese displays あ for hiragana. For Japanese character choices, switch to Japanese, type a word such as `nihongo`, then press Space twice. Press Enter to accept your choice. If you also have multiple keyboard layouts, right-click the indicator to cycle those. The keyboard indicator appears on the right of the bar when you have multiple keyboard layouts or input methods. Click it to switch input languages; Japanese displays あ for hiragana. For Japanese character choices, switch to Japanese, type a word such as `nihongo`, then press Space twice. Press Enter to accept your choice. If you also have multiple keyboard layouts, right-click the indicator to cycle those.
Use _Setup > Typing > Keyboard Layouts_ to select layouts such as English and French, including keyboard variants. Use _Setup > Typing > Input Methods_ to select which composition engines are active. Both pickers open inside the Omarchy menu with your current selections checked. Click or press Enter to toggle a choice, then select Apply to save. Deselecting an input method removes it from the cycle without uninstalling it; the keyboard remains available. Use _Setup > Typing > Keyboard Layouts_ to select layouts such as English and French, including keyboard variants. Use _Setup > Typing > Input Methods_ to select which composition engines are active. Both pickers open inside the Omarchy menu with your current selections checked. Click or press Enter to toggle a choice; each change applies immediately. Deselecting an input method removes it from the cycle without uninstalling it; the keyboard remains available.
Run `omarchy setup keyboard` or `omarchy setup input` to open the same pickers. To append one language from the command line, use `omarchy setup input mozc`, `hangul`, `pinyin`, or `chewing`. Your custom switching keys are preserved. For alternative engine layouts or keys, install `fcitx5-configtool` with `omarchy pkg add fcitx5-configtool` and launch it from a terminal. Selecting an input language does not change the desktop language. Run `omarchy setup keyboard` or `omarchy setup input` to open the same pickers. To append one language from the command line, use `omarchy setup input mozc`, `hangul`, `pinyin`, or `chewing`. Your custom switching keys are preserved. For alternative engine layouts or keys, install `fcitx5-configtool` with `omarchy pkg add fcitx5-configtool` and launch it from a terminal. Selecting an input language does not change the desktop language.
+38 -3
View File
@@ -59,6 +59,7 @@ Item {
property var dmenuOptions: [] property var dmenuOptions: []
property bool dmenuMultiple: false property bool dmenuMultiple: false
property var dmenuSelected: [] property var dmenuSelected: []
property var dmenuOnChange: []
property string selectionFile: "" property string selectionFile: ""
property string doneFile: "" property string doneFile: ""
property int dmenuWidth: 300 property int dmenuWidth: 300
@@ -565,7 +566,7 @@ Item {
return return
} }
if (root.dmenuMultiple) { if (root.dmenuMultiple && root.dmenuOnChange.length === 0) {
displayModel.append({ displayModel.append({
itemId: "dmenu.apply", disabled: false, kind: "dmenu", icon: "", iconFont: "", itemId: "dmenu.apply", disabled: false, kind: "dmenu", icon: "", iconFont: "",
appIcon: "", appId: "", label: "Apply", target: "", appIcon: "", appId: "", label: "Apply", target: "",
@@ -616,6 +617,18 @@ Item {
}) })
} }
function updateDmenuChecks() {
for (var i = 0; i < displayModel.count; i++) {
var row = displayModel.get(i)
if (row.itemId === "dmenu.apply") {
displayModel.setProperty(i, "detail", root.dmenuSelected.length + " selected")
} else if (row.kind === "dmenu") {
var value = MenuModel.dmenuValue(root.dmenuOptions[Number(row.itemId.substring(6))])
displayModel.setProperty(i, "icon", root.dmenuSelected.indexOf(value) !== -1 ? "✓" : "○")
}
}
}
function rebuildDisplay() { function rebuildDisplay() {
if (root.dmenuActive) { if (root.dmenuActive) {
root.rebuildDmenuDisplay() root.rebuildDmenuDisplay()
@@ -784,8 +797,17 @@ Item {
root.applyDmenuSelection(JSON.stringify(MenuModel.dmenuSelections(root.dmenuOptions, root.dmenuSelected))) root.applyDmenuSelection(JSON.stringify(MenuModel.dmenuSelections(root.dmenuOptions, root.dmenuSelected)))
} else { } else {
var value = MenuModel.dmenuValue(root.dmenuOptions[Number(picked.itemId.substring(6))]) var value = MenuModel.dmenuValue(root.dmenuOptions[Number(picked.itemId.substring(6))])
root.dmenuSelected = MenuModel.toggleDmenuSelection(root.dmenuSelected, value) var selected = MenuModel.toggleDmenuSelection(root.dmenuSelected, value)
root.rebuildDmenuDisplay() if (root.dmenuOnChange.length > 0) {
if (selectionProc.running) return
selectionProc.serial = root.requestSerial
selectionProc.selected = MenuModel.dmenuSelections(root.dmenuOptions, selected)
selectionProc.command = root.dmenuOnChange.concat([JSON.stringify(selectionProc.selected)])
selectionProc.running = true
} else {
root.dmenuSelected = selected
root.updateDmenuChecks()
}
} }
} else { } else {
root.applyDmenuSelection(picked.detail ? picked.label + "\t" + picked.detail : picked.label) root.applyDmenuSelection(picked.detail ? picked.label + "\t" + picked.detail : picked.label)
@@ -888,6 +910,8 @@ Item {
dmenuPrompt = String(payload.prompt || (mode === "input" ? "Input" : "Select")) dmenuPrompt = String(payload.prompt || (mode === "input" ? "Input" : "Select"))
dmenuOptions = Array.isArray(payload.options) ? payload.options : [] dmenuOptions = Array.isArray(payload.options) ? payload.options : []
dmenuMultiple = mode === "select" && payload.multiple === true dmenuMultiple = mode === "select" && payload.multiple === true
dmenuOnChange = dmenuMultiple && Array.isArray(payload.onChange)
&& payload.onChange.every(function(arg) { return typeof arg === "string" }) ? payload.onChange : []
dmenuSelected = MenuModel.dmenuSelections(dmenuOptions, Array.isArray(payload.selected) ? payload.selected : []) dmenuSelected = MenuModel.dmenuSelections(dmenuOptions, Array.isArray(payload.selected) ? payload.selected : [])
selectionFile = String(payload.selectionFile || "") selectionFile = String(payload.selectionFile || "")
doneFile = String(payload.doneFile || "") doneFile = String(payload.doneFile || "")
@@ -965,6 +989,17 @@ Item {
} }
} }
Process {
id: selectionProc
property int serial: 0
property var selected: []
onExited: function(exitCode) {
if (serial !== root.requestSerial || !root.dmenuActive) return
if (exitCode === 0) root.dmenuSelected = selected
root.updateDmenuChecks()
}
}
Process { Process {
id: resultProc id: resultProc
onExited: { onExited: {
@@ -153,3 +153,5 @@ TEST_INPUT_PREFERENCE=$'INPUT_METHOD=hangul\nXKB_LAYOUT=kr\n' assert_input "a la
TEST_KEYBOARD_SELECTION=$'XKBLAYOUT=us,fr\nXKBVARIANT=intl,\n' assert_input "menu selections replace installer desktop defaults" "[us,fr] [intl,] [$base_options]" $'XKBLAYOUT=jp\n' TEST_KEYBOARD_SELECTION=$'XKBLAYOUT=us,fr\nXKBVARIANT=intl,\n' assert_input "menu selections replace installer desktop defaults" "[us,fr] [intl,] [$base_options]" $'XKBLAYOUT=jp\n'
TEST_KEYBOARD_SELECTION=$'XKBLAYOUT=us,fr\nXKBVARIANT=intl,\n' assert_greeter_input "menu selections leave the greeter keyboard unchanged" "[jp] [] []" $'XKBLAYOUT=jp\n' TEST_KEYBOARD_SELECTION=$'XKBLAYOUT=us,fr\nXKBVARIANT=intl,\n' assert_greeter_input "menu selections leave the greeter keyboard unchanged" "[jp] [] []" $'XKBLAYOUT=jp\n'
TEST_KEYBOARD_SELECTION=$'XKBLAYOUT=us,ru\nXKBVARIANT=,phonetic\n' assert_input "saved non-Latin selections retain the layout shortcut" "[us,ru] [,phonetic] [$toggle_options]" $'XKBLAYOUT=us\n'
+10 -2
View File
@@ -19,6 +19,7 @@ class TypingSetupTest(unittest.TestCase):
def test_variants_and_installer_binding_safety(self): def test_variants_and_installer_binding_safety(self):
self.assertEqual(typing.keyboard_values(["us:intl", "fr"]), ("us,fr", "intl,")) self.assertEqual(typing.keyboard_values(["us:intl", "fr"]), ("us,fr", "intl,"))
self.assertEqual(typing.keyboard_values(["ru:phonetic"]), ("us,ru", ",phonetic")) self.assertEqual(typing.keyboard_values(["ru:phonetic"]), ("us,ru", ",phonetic"))
self.assertEqual(typing.keyboard_values(["ru:phonetic", "us"]), ("us,ru", ",phonetic"))
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
typing.keyboard_values([]) typing.keyboard_values([])
@@ -47,7 +48,7 @@ class TypingSetupTest(unittest.TestCase):
def test_picker_preselects_current_entries_and_returns_empty_selection(self): def test_picker_preselects_current_entries_and_returns_empty_selection(self):
result = subprocess.CompletedProcess([], 0, stdout="[]") result = subprocess.CompletedProcess([], 0, stdout="[]")
with patch.object(typing.subprocess, "run", return_value=result) as process: with patch.object(typing.subprocess, "run", return_value=result) as process:
self.assertEqual(typing.choose("Inputs", {"mozc": "Japanese", "hangul": "Korean"}, ["hangul"]), []) self.assertEqual(typing.choose("Inputs", {"mozc": "Japanese", "hangul": "Korean"}, ["hangul"], "input"), [])
args = process.call_args.args[0] args = process.call_args.args[0]
self.assertEqual(args[2], "\tKorean") self.assertEqual(args[2], "\tKorean")
self.assertIn("--multiple", args) self.assertIn("--multiple", args)
@@ -56,7 +57,14 @@ class TypingSetupTest(unittest.TestCase):
def test_picker_rejects_unknown_returned_values(self): def test_picker_rejects_unknown_returned_values(self):
with patch.object(typing.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, stdout='["unknown"]')): with patch.object(typing.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, stdout='["unknown"]')):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
typing.choose("Inputs", {"mozc": "Japanese"}, []) typing.choose("Inputs", {"mozc": "Japanese"}, [], "input")
def test_immediate_selection_removes_an_input_and_accepts_keyboard_only(self):
with patch.object(typing, "save_inputs") as save:
typing.apply_selection("input", {"mozc": "Japanese", "hangul": "Korean"}, ["Japanese"])
save.assert_called_once_with(["mozc"])
typing.apply_selection("input", {"mozc": "Japanese"}, [])
self.assertEqual(save.call_args.args, ([],))
def test_keyboard_override_failure_preserves_symlink_and_restores_settings(self): def test_keyboard_override_failure_preserves_symlink_and_restores_settings(self):
with tempfile.TemporaryDirectory() as temporary: with tempfile.TemporaryDirectory() as temporary: