diff --git a/scripts/make-kbd-info.py b/scripts/make-kbd-info.py new file mode 100755 index 00000000..9a6cb81d --- /dev/null +++ b/scripts/make-kbd-info.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 + +from collections import defaultdict +import os +import shutil +import subprocess + +from subiquity.common.serialize import Serializer +from subiquity.common.types import ( + KeyboardLayout, + KeyboardVariant, + ) + +tdir = os.path.join(os.environ.get('SNAPCRAFT_PART_INSTALL', '.'), 'kbds') +if os.path.exists(tdir): + shutil.rmtree(tdir) +os.mkdir(tdir) + +p = subprocess.Popen( + ['/usr/share/console-setup/kbdnames-maker', + '/usr/share/console-setup/KeyboardNames.pl'], + stdout=subprocess.PIPE, encoding='utf-8') + + +lang_to_layouts = defaultdict(dict) + + +for line in p.stdout: + lang, element, name, value = line.strip().split("*", 3) + if element == 'model': + continue + elif element == 'variant': + layout = lang_to_layouts[lang][name] + variant, value = value.split('*', 1) + if not layout.variants and variant != "": + raise Exception( + "subiquity assumes all keyboard layouts have the default " + "variant at index 0!") + layout.variants.append(KeyboardVariant(code=variant, name=value)) + elif element == 'layout': + lang_to_layouts[lang][name] = KeyboardLayout( + code=name, name=value, variants=[]) + + +s = Serializer(compact=True) + + +for lang, layouts in lang_to_layouts.items(): + if 'us' not in layouts: + raise Exception("subiquity assumes there is always a us keyboard " + "layout") + outpath = os.path.join(tdir, lang + '.jsonl') + with open(outpath, 'w') as out: + for layout in layouts.values(): + if len(layout.variants) == 0: + raise Exception( + "subiquity assumes all keyboard layouts have at least one " + "variant!") + out.write(s.to_json(KeyboardLayout, layout) + "\n") diff --git a/snapcraft.yaml b/snapcraft.yaml index 2b019e20..4390dd51 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -122,19 +122,20 @@ parts: stage: - users-and-groups - reserved-usernames - kbdnames: + keyboard-data: plugin: nil build-packages: - console-setup - locales - xkb-data-i18n - override-build: | - /usr/share/console-setup/kbdnames-maker /usr/share/console-setup/KeyboardNames.pl > $SNAPCRAFT_PART_INSTALL/kbdnames.txt + - python3-attr + override-build: PYTHONPATH=$SNAPCRAFT_PROJECT_DIR/ $SNAPCRAFT_PROJECT_DIR/scripts/make-kbd-info.py stage: - - kbdnames.txt + - kbds/ font: plugin: dump source: ./ + source-type: git organize: font/subiquity.psf: subiquity.psf stage: diff --git a/subiquity/client/keyboard.py b/subiquity/client/keyboard.py index 12c9ba06..228c5ec7 100644 --- a/subiquity/client/keyboard.py +++ b/subiquity/client/keyboard.py @@ -13,10 +13,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from collections import defaultdict import os -from subiquity.common.types import KeyboardSetting +from subiquity.common.serialize import Serializer +from subiquity.common.types import KeyboardLayout, KeyboardSetting # Non-latin keyboard layouts that are handled in a uniform way @@ -107,14 +107,15 @@ def for_ui(setting): class KeyboardList: def __init__(self): - self._kbnames_file = os.path.join( - os.environ.get("SNAP", '.'), - 'kbdnames.txt') + self._kbnames_dir = os.path.join(os.environ.get("SNAP", '.'), 'kbds') + self.serializer = Serializer(compact=True) self._clear() + def _file_for_lang(self, code): + return os.path.join(self._kbnames_dir, code + '.jsonl') + def has_language(self, code): - self.load_language(code) - return bool(self.layouts) + return os.path.exists(self._file_for_lang(code)) def load_language(self, code): if code == self.current_lang: @@ -122,33 +123,13 @@ class KeyboardList: self._clear() - with open(self._kbnames_file, encoding='utf-8') as kbdnames: - self._load_file(code, kbdnames) + with open(self._file_for_lang(code)) as kbdnames: + self.layouts = [ + self.serializer.from_json(KeyboardLayout, line) + for line in kbdnames + ] self.current_lang = code def _clear(self): self.current_lang = None - self.layouts = {} - self.variants = defaultdict(dict) - - def _load_file(self, code, kbdnames): - for line in kbdnames: - line = line.rstrip('\n') - got_lang, element, name, value = line.split("*", 3) - if got_lang != code: - continue - - if element == "layout": - self.layouts[name] = value - elif element == "variant": - variantname, variantdesc = value.split("*", 1) - self.variants[name][variantname] = variantdesc - - def lookup(self, code): - if ':' in code: - layout_code, variant_code = code.split(":", 1) - layout = self.layouts.get(layout_code, '?') - variant = self.variants.get(layout_code, {}).get(variant_code, '?') - return (layout, variant) - else: - return self.layouts.get(code, '?'), None + self.layouts = [] diff --git a/subiquity/common/types.py b/subiquity/common/types.py index 30b63f69..cea4712b 100644 --- a/subiquity/common/types.py +++ b/subiquity/common/types.py @@ -99,6 +99,19 @@ class KeyboardSetting: toggle: Optional[str] = None +@attr.s(auto_attribs=True) +class KeyboardVariant: + code: str + name: str + + +@attr.s(auto_attribs=True) +class KeyboardLayout: + code: str + name: str + variants: List[KeyboardVariant] + + @attr.s(auto_attribs=True) class ZdevInfo: id: str diff --git a/subiquity/ui/views/keyboard.py b/subiquity/ui/views/keyboard.py index 90bdf983..eadf3f04 100644 --- a/subiquity/ui/views/keyboard.py +++ b/subiquity/ui/views/keyboard.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import locale import logging from urwid import ( @@ -44,7 +45,7 @@ from subiquitycore.ui.stretchy import ( from subiquitycore.ui.utils import button_pile, Color, Padding, screen from subiquitycore.view import BaseView -from subiquity.client.keyboard import latinizable, for_ui +from subiquity.client.keyboard import for_ui, latinizable from subiquity.common.types import KeyboardSetting from subiquity.ui.views import pc105 @@ -127,21 +128,23 @@ another layout or run the automated detection again. """) def ok(self, sender): - self.keyboard_detector.keyboard_view.found_layout(self.step.result) + self.keyboard_detector.keyboard_view.found_layout( + self.layout, self.variant) def make_body(self): - kl = self.keyboard_detector.keyboard_view.keyboard_list - layout, variant = kl.lookup(self.step.result) - var_desc = [] + if ':' in self.step.result: + layout_code, variant_code = self.step.result.split(':') + else: + layout_code, variant_code = self.step.result, "" + view = self.keyboard_detector.keyboard_view + self.layout, self.variant = view.lookup(layout_code, variant_code) layout_text = _("Layout") var_text = _("Variant") width = max(len(layout_text), len(var_text), 12) - if variant is not None: - var_desc = [Text("%*s: %s" % (width, var_text, variant))] return Pile([ Text(_(self.preamble)), - Text("%*s: %s" % (width, layout_text, layout)), - ] + var_desc + [ + Text("%*s: %s" % (width, layout_text, self.layout.name)), + Text("%*s: %s" % (width, var_text, self.variant.name)), Text(_(self.postamble)), button_pile([ok_btn(label=_("OK"), on_press=self.ok)]), ]) @@ -394,25 +397,16 @@ class KeyboardView(BaseView): self.form = KeyboardForm() opts = [] - for layout, desc in self.keyboard_list.layouts.items(): - opts.append(Option((desc, True, layout))) - opts.sort(key=lambda o: o.label.text) + for layout in self.keyboard_list.layouts: + opts.append(Option((layout.name, True, layout))) + opts.sort(key=lambda o: locale.strxfrm(o.label.text)) connect_signal(self.form, 'submit', self.done) connect_signal(self.form, 'cancel', self.cancel) connect_signal(self.form.layout.widget, "select", self.select_layout) self.form.layout.widget.options = opts setting = for_ui(initial_setting) - try: - self.form.layout.widget.value = setting.layout - except AttributeError: - # Don't crash on pre-existing invalid config. - pass - self.select_layout(None, setting.layout) - try: - self.form.variant.widget.value = setting.variant - except AttributeError: - # Don't crash on pre-existing invalid config. - pass + layout, variant = self.lookup(setting.layout, setting.variant) + self.set_values(layout, variant) if self.controller.opts.run_on_serial: excerpt = _('Please select the layout of the keyboard directly ' @@ -437,32 +431,24 @@ class KeyboardView(BaseView): narrow_rows=True)) def detect(self, sender): - detector = Detector(self) - detector.start() + Detector(self).start() - def found_layout(self, result): + def found_layout(self, layout, variant): self.remove_overlay() - log.debug("found_layout %s", result) - if ':' in result: - layout, variant = result.split(':') - else: - layout, variant = result, "" - self.form.layout.widget.value = layout - self.select_layout(None, layout) - self.form.variant.widget.value = variant + log.debug("found_layout %r %r", layout.code, variant.code) + self.set_values(layout, variant) self._w.base_widget.focus_position = 4 def done(self, result): - layout = self.form.layout.widget.value - variant = '' - if self.form.variant.widget.value is not None: - variant = self.form.variant.widget.value - setting = KeyboardSetting(layout=layout, variant=variant) - new_setting = latinizable(setting) - if new_setting != setting: - self.show_stretchy_overlay(ToggleQuestion(self, new_setting)) - return - self.really_done(setting) + data = result.as_data() + layout = data['layout'] + variant = data.get('variant', layout.variants[0]) + setting = KeyboardSetting(layout=layout.code, variant=variant.code) + other = latinizable(setting) + if setting != other: + self.show_stretchy_overlay(ToggleQuestion(self, other)) + else: + self.really_done(setting) def really_done(self, setting): apply = False @@ -479,18 +465,29 @@ class KeyboardView(BaseView): if sender is not None: log.debug("select_layout %s", layout) opts = [] - default_i = -1 - layout_items = enumerate(self.keyboard_list.variants[layout].items()) - for i, (variant, variant_desc) in layout_items: - if variant == "": - default_i = i - opts.append(Option((variant_desc, True, variant))) - opts.sort(key=lambda o: o.label.text) - if default_i < 0: - opts.insert(0, Option(("default", True, ""))) + for variant in layout.variants: + opts.append(Option((variant.name, True, variant))) + # ./scripts/make-kbd-info.py checks that the default is always + # at index 0 + opts[1:] = sorted(opts[1:], key=lambda o: locale.strxfrm(o.label.text)) self.form.variant.widget.options = opts - if default_i < 0: - self.form.variant.widget.index = 0 - else: - self.form.variant.widget.index = default_i + self.form.variant.widget.index = 0 self.form.variant.enabled = len(opts) > 1 + + def lookup(self, layout_code, variant_code): + for layout in self.keyboard_list.layouts: + if layout.code == layout_code: + break + if layout.code == "us": + default = layout + else: + layout = default + for variant in layout.variants: + if variant.code == variant_code: + return layout, variant + return layout, layout.variants[0] + + def set_values(self, layout, variant): + self.form.layout.widget.value = layout + self.select_layout(None, layout) + self.form.variant.widget.value = variant