Use installer keyboard choices and keep typing selections accurate
This commit is contained in:
1 parent
9421e961ea
commit
e210b47e33
7 files changed
+94
-43
No files matched your search
@@ -10,16 +10,30 @@ import sys
|
||||
import configure as setup
|
||||
|
||||
|
||||
def controller(method):
|
||||
return json.loads(setup.run(setup.CONTROLLER[:1] + ["--json=short"] + setup.CONTROLLER[1:] + [method]))["data"][0]
|
||||
|
||||
|
||||
def keyboard_catalog():
|
||||
# Read the same choices as the ISO and first-boot form, then convert console
|
||||
# keymaps through systemd's mapping, just as localectl does during install.
|
||||
choices = setup.run(["bash", "-c", 'source "$1"; printf "%s\\n" "$OMARCHY_KEYBOARD_LAYOUTS"',
|
||||
"bash", str(setup.ROOT / "install/provisioning/setup-form.sh")])
|
||||
mappings = {}
|
||||
for line in Path("/usr/share/systemd/kbd-model-map").read_text().splitlines():
|
||||
fields = line.split()
|
||||
if not fields or fields[0].startswith("#"):
|
||||
continue
|
||||
layout = fields[1].split(",")[0]
|
||||
variant = fields[3].split(",")[0]
|
||||
mappings.setdefault(fields[0], layout + (":" + variant if variant != "-" else ""))
|
||||
# These console names have no entry in systemd's conversion table.
|
||||
mappings.update({"azerty": "az", "bg-cp1251": "bg", "colemak": "us:colemak", "kyrgyz": "kg",
|
||||
"de_CH-latin1": "ch", "no-latin1": "no"})
|
||||
catalog = {}
|
||||
for layout, label, languages, variants in controller("AvailableKeyboardLayouts"):
|
||||
catalog[layout] = label
|
||||
for variant, description, languages in variants:
|
||||
catalog[f"{layout}:{variant}"] = description
|
||||
for choice in choices.splitlines():
|
||||
label, keymap, *input_settings = choice.split("|")
|
||||
layout = input_settings[1] if len(input_settings) > 1 else mappings.get(keymap, keymap)
|
||||
if label == "Lao":
|
||||
layout = "la"
|
||||
# Composition engines belong in Input Methods, not as duplicate US layouts.
|
||||
catalog.setdefault(layout, label)
|
||||
return catalog
|
||||
|
||||
|
||||
@@ -39,11 +53,11 @@ def picker_rows(catalog, selected):
|
||||
return rows
|
||||
|
||||
|
||||
def choose(title, catalog, selected, kind):
|
||||
def choose(title, catalog, selected, kind, overrides=None):
|
||||
catalog = {**catalog, **{key: catalog.get(key, key) for key in selected}}
|
||||
rows = picker_rows(catalog, selected)
|
||||
args = ["omarchy-menu-select", title] + list(rows.values())
|
||||
on_change = ["python", str(setup.ROOT / "default/input-methods/typing.py"), "apply", kind, json.dumps(catalog)]
|
||||
on_change = ["python", str(setup.ROOT / "default/input-methods/typing.py"), "apply", kind, json.dumps({"catalog": catalog, "overrides": overrides or {}})]
|
||||
args += ["--", "--multiple", "--width", "620", "--maxheight", "650", "--on-change", json.dumps(on_change)]
|
||||
for key in selected:
|
||||
args += ["--selected", rows[key][1:]]
|
||||
@@ -52,11 +66,6 @@ def choose(title, catalog, selected, kind):
|
||||
return None
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.stderr.strip() or "The selection menu could not open")
|
||||
values = json.loads(result.stdout)
|
||||
valid = {row[1:]: key for key, row in rows.items()}
|
||||
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")
|
||||
return list(dict.fromkeys(valid[value] for value in values))
|
||||
|
||||
|
||||
def input_catalog(items):
|
||||
@@ -68,11 +77,11 @@ def input_catalog(items):
|
||||
return catalog, available
|
||||
|
||||
|
||||
def input_items(items, selected):
|
||||
def input_items(items, selected, overrides=None):
|
||||
keyboard = [item for item in items if item[0].startswith("keyboard-")]
|
||||
if not keyboard:
|
||||
raise RuntimeError("The current input group has no keyboard input")
|
||||
existing = dict(items)
|
||||
existing = {**(overrides or {}), **dict(items)}
|
||||
return keyboard + [[name, existing.get(name, "")] for name in selected]
|
||||
|
||||
|
||||
@@ -94,10 +103,10 @@ def configure_inputs():
|
||||
group, layout, items = setup.live_group()
|
||||
catalog, available = input_catalog(items)
|
||||
current = [name for name, override in items if not name.startswith("keyboard-")]
|
||||
choose("Input Methods", catalog, current, "input")
|
||||
choose("Input Methods", catalog, current, "input", dict(items))
|
||||
|
||||
|
||||
def save_inputs(selected):
|
||||
def save_inputs(selected, overrides=None):
|
||||
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]
|
||||
@@ -107,7 +116,7 @@ def save_inputs(selected):
|
||||
for name in selected:
|
||||
if name in setup.PRESETS:
|
||||
setup.font_default(config_home, name)
|
||||
set_inputs(group, layout, items, input_items(items, selected))
|
||||
set_inputs(group, layout, items, input_items(items, selected, overrides))
|
||||
|
||||
|
||||
def keyboard_values(selected):
|
||||
@@ -171,16 +180,19 @@ def configure_keyboard():
|
||||
choose("Keyboard Layouts", catalog, current, "keyboard")
|
||||
|
||||
|
||||
def apply_selection(kind, catalog, values):
|
||||
def apply_selection(kind, context, values):
|
||||
catalog = context["catalog"]
|
||||
rows = picker_rows(catalog, [])
|
||||
valid = {row[1:]: key for key, row in rows.items()}
|
||||
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)
|
||||
save_inputs(selected, context.get("overrides"))
|
||||
else:
|
||||
save_keyboard(selected)
|
||||
selected = keyboard_selection()
|
||||
print(json.dumps([rows[key][1:] for key in selected]))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -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.
|
||||
|
||||
`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.
|
||||
`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` uses the installer’s keyboard choices, converted to desktop layouts through systemd’s keymap table, and reads 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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
Use _Setup > Typing > Keyboard Layouts_ to select layouts such as English and French, from the same choices offered during installation. 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.
|
||||
|
||||
|
||||
@@ -800,8 +800,9 @@ Item {
|
||||
var selected = MenuModel.toggleDmenuSelection(root.dmenuSelected, value)
|
||||
if (root.dmenuOnChange.length > 0) {
|
||||
if (selectionProc.running) return
|
||||
selectionProc.collected = ""
|
||||
selectionProc.serial = root.requestSerial
|
||||
selectionProc.selected = MenuModel.dmenuSelections(root.dmenuOptions, selected)
|
||||
selectionProc.selected = MenuModel.dmenuSelections(root.dmenuOptions, selected, true)
|
||||
selectionProc.command = root.dmenuOnChange.concat([JSON.stringify(selectionProc.selected)])
|
||||
selectionProc.running = true
|
||||
} else {
|
||||
@@ -912,7 +913,7 @@ Item {
|
||||
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 : [], dmenuOnChange.length > 0)
|
||||
selectionFile = String(payload.selectionFile || "")
|
||||
doneFile = String(payload.doneFile || "")
|
||||
requestActive = !!doneFile
|
||||
@@ -993,9 +994,18 @@ Item {
|
||||
id: selectionProc
|
||||
property int serial: 0
|
||||
property var selected: []
|
||||
property string collected: ""
|
||||
stdout: StdioCollector { onStreamFinished: selectionProc.collected = text }
|
||||
onExited: function(exitCode) {
|
||||
if (serial !== root.requestSerial || !root.dmenuActive) return
|
||||
if (exitCode === 0) root.dmenuSelected = selected
|
||||
if (exitCode === 0) {
|
||||
var applied = selected
|
||||
if (collected.trim()) {
|
||||
try { applied = JSON.parse(collected) } catch (error) { return }
|
||||
if (!Array.isArray(applied)) return
|
||||
}
|
||||
root.dmenuSelected = MenuModel.dmenuSelections(root.dmenuOptions, applied, true)
|
||||
}
|
||||
root.updateDmenuChecks()
|
||||
}
|
||||
}
|
||||
@@ -1282,6 +1292,16 @@ Item {
|
||||
spacing: root.rowSpacing
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
WheelHandler {
|
||||
target: null
|
||||
onWheel: function(event) {
|
||||
var delta = event.pixelDelta.y || event.angleDelta.y / 120 * root.rowHeightForDetail("") * 3
|
||||
var bottom = resultList.originY + Math.max(0, resultList.contentHeight - resultList.height)
|
||||
resultList.contentY = Math.max(resultList.originY, Math.min(bottom, resultList.contentY - delta))
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
section.property: "section"
|
||||
section.criteria: ViewSection.FullString
|
||||
section.delegate: Item {
|
||||
|
||||
@@ -509,9 +509,10 @@ function dmenuValue(option) {
|
||||
return parts.join("\t")
|
||||
}
|
||||
|
||||
function dmenuSelections(options, selected) {
|
||||
return options.map(dmenuValue).filter(function(value, index, values) {
|
||||
return selected.indexOf(value) !== -1 && values.indexOf(value) === index
|
||||
function dmenuSelections(options, selected, preserveOrder) {
|
||||
var available = options.map(dmenuValue)
|
||||
return (preserveOrder ? selected : available).filter(function(value, index, values) {
|
||||
return available.indexOf(value) !== -1 && selected.indexOf(value) !== -1 && values.indexOf(value) === index
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,22 @@ class TypingSetupTest(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
typing.keyboard_values([])
|
||||
|
||||
def test_keyboard_catalog_includes_variants(self):
|
||||
with patch.object(typing, "controller", return_value=[["us", "English (US)", ["en"], [["intl", "English (US, international)", ["en"]]]]]):
|
||||
self.assertEqual(typing.keyboard_catalog(), {"us": "English (US)", "us:intl": "English (US, international)"})
|
||||
def test_keyboard_catalog_uses_installer_choices(self):
|
||||
catalog = typing.keyboard_catalog()
|
||||
self.assertEqual(catalog["us"], "English (US)")
|
||||
self.assertEqual(catalog["gb"], "English (UK)")
|
||||
self.assertEqual(catalog["us:colemak"], "English (US, Colemak)")
|
||||
self.assertEqual(catalog["ch"], "German (Switzerland)")
|
||||
self.assertEqual(catalog["la"], "Lao")
|
||||
self.assertEqual(catalog["latam"], "Spanish (Latin American)")
|
||||
self.assertNotIn("us:intl", catalog)
|
||||
self.assertLess(len(catalog), 60)
|
||||
|
||||
def test_readding_input_keeps_its_original_override(self):
|
||||
self.assertEqual(typing.input_items([["keyboard-us", ""]], ["mozc"], {"mozc": "jp"}),
|
||||
[["keyboard-us", ""], ["mozc", "jp"]])
|
||||
self.assertEqual(typing.input_items([["keyboard-us", ""], ["mozc", "us"]], ["mozc"], {"mozc": "jp"}),
|
||||
[["keyboard-us", ""], ["mozc", "us"]])
|
||||
|
||||
def test_input_deselection_keeps_keyboard_and_selected_overrides(self):
|
||||
items = [["keyboard-us", ""], ["mozc", "jp"], ["hangul", ""], ["custom", "de"]]
|
||||
@@ -48,23 +61,27 @@ class TypingSetupTest(unittest.TestCase):
|
||||
def test_picker_preselects_current_entries_and_returns_empty_selection(self):
|
||||
result = subprocess.CompletedProcess([], 0, stdout="[]")
|
||||
with patch.object(typing.subprocess, "run", return_value=result) as process:
|
||||
self.assertEqual(typing.choose("Inputs", {"mozc": "Japanese", "hangul": "Korean"}, ["hangul"], "input"), [])
|
||||
self.assertIsNone(typing.choose("Inputs", {"mozc": "Japanese", "hangul": "Korean"}, ["hangul"], "input"))
|
||||
args = process.call_args.args[0]
|
||||
self.assertEqual(args[2], "\tKorean")
|
||||
self.assertIn("--multiple", args)
|
||||
self.assertEqual(args[-2:], ["--selected", "Korean"])
|
||||
|
||||
def test_picker_rejects_unknown_returned_values(self):
|
||||
with patch.object(typing.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, stdout='["unknown"]')):
|
||||
with self.assertRaises(ValueError):
|
||||
typing.choose("Inputs", {"mozc": "Japanese"}, [], "input")
|
||||
def test_apply_rejects_unknown_values(self):
|
||||
with self.assertRaises(ValueError):
|
||||
typing.apply_selection("input", {"catalog": {"mozc": "Japanese"}}, ["unknown"])
|
||||
|
||||
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, ([],))
|
||||
with patch.object(typing, "save_inputs") as save, patch("builtins.print"):
|
||||
typing.apply_selection("input", {"catalog": {"mozc": "Japanese", "hangul": "Korean"}}, ["Japanese"])
|
||||
save.assert_called_once_with(["mozc"], None)
|
||||
typing.apply_selection("input", {"catalog": {"mozc": "Japanese"}}, [])
|
||||
self.assertEqual(save.call_args.args, ([], None))
|
||||
|
||||
def test_keyboard_checks_reflect_normalized_selection(self):
|
||||
with patch.object(typing, "save_keyboard"), patch.object(typing, "keyboard_selection", return_value=["us", "ru"]), patch("builtins.print") as output:
|
||||
typing.apply_selection("keyboard", {"catalog": {"us": "English", "ru": "Russian"}}, ["Russian"])
|
||||
self.assertEqual(json.loads(output.call_args.args[0]), ["English", "Russian"])
|
||||
|
||||
def test_keyboard_override_failure_preserves_symlink_and_restores_settings(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
|
||||
@@ -10,6 +10,7 @@ const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
|
||||
const options = ['French', '\tJapanese (Mozc)', 'x\tSame label\tfirst', 'x\tSame label\tsecond']
|
||||
assertEqual(menu.dmenuValue(options[2]), 'Same label\tfirst', 'multi-select keeps a stable subtext value without the icon')
|
||||
assertDeepEqual(menu.dmenuSelections(options, ['Same label\tsecond', 'French', 'stale']), ['French', 'Same label\tsecond'], 'multi-select preserves option order and excludes stale choices')
|
||||
assertDeepEqual(menu.dmenuSelections(options, ['Same label\tsecond', 'French', 'stale'], true), ['Same label\tsecond', 'French'], 'immediate selections preserve switching order')
|
||||
assertDeepEqual(menu.toggleDmenuSelection(['French'], 'French'), [], 'deselecting the final option produces an empty selection')
|
||||
assertDeepEqual(menu.toggleDmenuSelection(['French'], 'Japanese (Mozc)'), ['French', 'Japanese (Mozc)'], 'selecting an input retains earlier choices')
|
||||
JS
|
||||
Reference in new issue
Block a user