pre-process keyboard auto detection steps into API friendly format

Continuing the theme of previous work, this branch parses pc105.tree
into API-friendly attr-based classes at snap build time (which also lets
us check some constraints then and not at run time).
This commit is contained in:
Michael Hudson-Doyle 2021-03-16 09:57:47 +13:00
parent 252ec6d11e
commit fb649bf7d5
6 changed files with 137 additions and 103 deletions

View File

@ -39,6 +39,7 @@ subiquity/common/serialize.py
subiquity/common/tests/__init__.py subiquity/common/tests/__init__.py
subiquity/common/tests/test_filesystem.py subiquity/common/tests/test_filesystem.py
subiquity/common/tests/test_keyboard.py subiquity/common/tests/test_keyboard.py
subiquity/common/tests/test_serialization.py
subiquity/common/types.py subiquity/common/types.py
subiquitycore/async_helpers.py subiquitycore/async_helpers.py
subiquitycore/contextlib38.py subiquitycore/contextlib38.py
@ -165,7 +166,6 @@ subiquity/ui/views/__init__.py
subiquity/ui/views/installprogress.py subiquity/ui/views/installprogress.py
subiquity/ui/views/keyboard.py subiquity/ui/views/keyboard.py
subiquity/ui/views/mirror.py subiquity/ui/views/mirror.py
subiquity/ui/views/pc105.py
subiquity/ui/views/proxy.py subiquity/ui/views/proxy.py
subiquity/ui/views/refresh.py subiquity/ui/views/refresh.py
subiquity/ui/views/snaplist.py subiquity/ui/views/snaplist.py

View File

@ -4,13 +4,23 @@ from collections import defaultdict
import os import os
import shutil import shutil
import subprocess import subprocess
import sys
from typing import Dict
from subiquity.common.serialize import Serializer from subiquity.common.serialize import Serializer
from subiquity.common.types import ( from subiquity.common.types import (
AnyStep,
KeyboardLayout, KeyboardLayout,
KeyboardVariant, KeyboardVariant,
StepPressKey,
StepKeyPresent,
) )
sys.path.insert(0, os.path.dirname(__file__))
import pc105 # noqa
tdir = os.path.join(os.environ.get('SNAPCRAFT_PART_INSTALL', '.'), 'kbds') tdir = os.path.join(os.environ.get('SNAPCRAFT_PART_INSTALL', '.'), 'kbds')
if os.path.exists(tdir): if os.path.exists(tdir):
shutil.rmtree(tdir) shutil.rmtree(tdir)
@ -57,3 +67,27 @@ for lang, layouts in lang_to_layouts.items():
"subiquity assumes all keyboard layouts have at least one " "subiquity assumes all keyboard layouts have at least one "
"variant!") "variant!")
out.write(s.to_json(KeyboardLayout, layout) + "\n") out.write(s.to_json(KeyboardLayout, layout) + "\n")
pc105tree = pc105.PC105Tree()
pc105tree.read_steps()
if '0' not in pc105tree.steps:
raise Exception("subiquity assumes there is a step '0' in the pc105 steps")
for index, step in pc105tree.steps.items():
if isinstance(step, StepPressKey):
if len(step.symbols) == 0 or len(step.keycodes) == 0:
raise Exception(f"step {index} {step} appears to be incomplete")
for v in step.keycodes.values():
if v not in pc105tree.steps:
raise Exception(
f"step {index} {step} references missing step {v}")
elif isinstance(step, StepKeyPresent):
for v in step.yes, step.no:
if v not in pc105tree.steps:
raise Exception(
f"step {index} {step} references missing step {v}")
with open(os.path.join(tdir, 'pc105.json'), 'w') as out:
out.write(s.to_json(Dict[str, AnyStep], pc105tree.steps))

View File

@ -17,113 +17,94 @@
# /usr/share/console-setup/pc105.tree. This code parses that data into # /usr/share/console-setup/pc105.tree. This code parses that data into
# subclasses of Step. # subclasses of Step.
"""Parses the pc105.tree file into Steps"""
class Step: from subiquity.common.types import (
def __repr__(self): StepKeyPresent,
kvs = [] StepPressKey,
for k, v in self.__dict__.items(): StepResult,
kvs.append("%s=%r" % (k, v)) )
return "%s(%s)" % (self.__class__.__name__, ", ".join(sorted(kvs)))
def check(self):
pass
class StepPressKey(Step):
# "Press one of the following keys"
def __init__(self):
self.symbols = []
self.keycodes = {}
def check(self):
if len(self.symbols) == 0 or len(self.keycodes) == 0:
raise Exception
class StepKeyPresent(Step):
# "Is this symbol present on your keyboard"
def __init__(self, symbol):
self.symbol = symbol
self.yes = None
self.no = None
def check(self):
if self.yes is None or self.no is None:
raise Exception
class StepResult(Step):
# "This is the autodetected layout"
def __init__(self, result):
self.result = result
class PC105Tree: class PC105Tree:
"""Parses the pc105.tree file into subclasses of Step"""
# This is adapted (quite heavily) from the code in ubiquity. # This is adapted (quite heavily) from the code in ubiquity.
def __init__(self): def __init__(self):
self.steps = {} self.steps = {}
def _add_step_from_lines(self, lines): def _add_step_from_lines(self, lines):
step = None step_cls = None
step_index = -1 args = None
step_index = None
for line in lines: for line in lines:
if line.startswith('STEP '): if line.startswith('STEP '):
step_index = int(line[5:]) step_index = line[5:].strip()
elif line.startswith('PRESS '): elif line.startswith('PRESS '):
# Ask the user to press a character on the keyboard. # Ask the user to press a character on the keyboard.
if step is None: if step_cls is None:
step = StepPressKey() step_cls = StepPressKey
elif not isinstance(step, StepPressKey): args = {
'symbols': [],
'keycodes': {},
}
elif step_cls is not StepPressKey:
raise Exception raise Exception
step.symbols.append(line[6:].strip()) args['symbols'].append(line[6:].strip())
elif line.startswith('CODE '): elif line.startswith('CODE '):
# Direct the evaluating code to process step ## next if the # Direct the evaluating code to process step ## next if the
# user has pressed a key which returned that keycode. # user has pressed a key which returned that keycode.
if not isinstance(step, StepPressKey): if step_cls is not StepPressKey:
raise Exception raise Exception
keycode = int(line[5:line.find(' ', 5)]) keycode = int(line[5:line.find(' ', 5)])
s = int(line[line.find(' ', 5) + 1:]) s = line[line.find(' ', 5) + 1:].strip()
step.keycodes[keycode] = s args['keycodes'][keycode] = s
elif line.startswith('FIND '): elif line.startswith('FIND '):
# Ask the user whether that character is present on their # Ask the user whether that character is present on their
# keyboard. # keyboard.
if step is None: if step_cls is None:
step = StepKeyPresent(line[5:].strip()) step_cls = StepKeyPresent
args = {
'symbol': line[5:].strip(),
}
else: else:
raise Exception raise Exception
elif line.startswith('FINDP '): elif line.startswith('FINDP '):
# Equivalent to FIND, except that the user is asked to consider # Equivalent to FIND, except that the user is asked to consider
# only the primary symbols (i.e. Plain and Shift). # only the primary symbols (i.e. Plain and Shift).
if step is None: if step_cls is None:
step = StepKeyPresent(line[6:].strip()) step_cls = StepKeyPresent
args = {'symbol': line[5:].strip()}
else: else:
raise Exception raise Exception
elif line.startswith('YES '): elif line.startswith('YES '):
# Direct the evaluating code to process step ## next if the # Direct the evaluating code to process step ## next if the
# user does have this key. # user does have this key.
if not isinstance(step, StepKeyPresent): if step_cls is not StepKeyPresent:
raise Exception raise Exception
step.yes = int(line[4:].strip()) args['yes'] = line[4:].strip()
elif line.startswith('NO '): elif line.startswith('NO '):
# Direct the evaluating code to process step ## next if the # Direct the evaluating code to process step ## next if the
# user does not have this key. # user does not have this key.
if not isinstance(step, StepKeyPresent): if step_cls is not StepKeyPresent:
raise Exception raise Exception
step.no = int(line[3:].strip()) args['no'] = line[3:].strip()
elif line.startswith('MAP '): elif line.startswith('MAP '):
# This step uniquely identifies a keymap. # This step uniquely identifies a keymap.
if step is None: if step_cls is None:
step = StepResult(line[4:].strip()) step_cls = StepResult
arg = line[4:].strip()
if ':' in arg:
layout, variant = arg.split(':')
else:
layout, variant = arg, ''
args = {'layout': layout, 'variant': variant}
else: else:
raise Exception raise Exception
else: else:
raise Exception raise Exception
if step is None or step_index == -1: if step_cls is None or step_index is None:
raise Exception raise Exception
step.check() self.steps[step_index] = step_cls(**args)
self.steps[step_index] = step
def read_steps(self): def read_steps(self):
cur_step_lines = [] cur_step_lines = []

View File

@ -14,9 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
from typing import Dict
from subiquity.common.serialize import Serializer from subiquity.common.serialize import Serializer
from subiquity.common.types import KeyboardLayout, KeyboardSetting from subiquity.common.types import (
AnyStep,
KeyboardLayout,
KeyboardSetting,
)
# Non-latin keyboard layouts that are handled in a uniform way # Non-latin keyboard layouts that are handled in a uniform way
@ -133,3 +138,8 @@ class KeyboardList:
def _clear(self): def _clear(self):
self.current_lang = None self.current_lang = None
self.layouts = [] self.layouts = []
def load_pc105(self):
path = os.path.join(self._kbnames_dir, 'pc105.json')
with open(path) as fp:
return self.serializer.from_json(Dict[str, AnyStep], fp.read())

View File

@ -20,7 +20,7 @@
import datetime import datetime
import enum import enum
import shlex import shlex
from typing import List, Optional from typing import Dict, List, Optional, Union
import attr import attr
@ -92,6 +92,31 @@ class RefreshStatus:
new_snap_version: str = '' new_snap_version: str = ''
@attr.s(auto_attribs=True)
class StepPressKey:
# "Press a key with one of the following symbols"
symbols: List[str]
keycodes: Dict[int, str]
@attr.s(auto_attribs=True)
class StepKeyPresent:
# "Is this symbol present on your keyboard"
symbol: str
yes: str
no: str
@attr.s(auto_attribs=True)
class StepResult:
# "This is the autodetected layout"
layout: str
variant: str
AnyStep = Union[StepPressKey, StepKeyPresent, StepResult]
@attr.s(auto_attribs=True) @attr.s(auto_attribs=True)
class KeyboardSetting: class KeyboardSetting:
layout: str layout: str

View File

@ -46,15 +46,18 @@ from subiquitycore.ui.utils import button_pile, Color, Padding, screen
from subiquitycore.view import BaseView from subiquitycore.view import BaseView
from subiquity.client.keyboard import for_ui, latinizable from subiquity.client.keyboard import for_ui, latinizable
from subiquity.common.types import KeyboardSetting from subiquity.common.types import (
from subiquity.ui.views import pc105 KeyboardSetting,
StepKeyPresent,
StepPressKey,
StepResult,
)
log = logging.getLogger("subiquity.ui.views.keyboard") log = logging.getLogger("subiquity.ui.views.keyboard")
class AutoDetectBase(WidgetWrap): class AutoDetectBase(WidgetWrap):
def __init__(self, keyboard_detector, step): def __init__(self, keyboard_detector, step):
# step is an instance of pc105.Step
self.keyboard_detector = keyboard_detector self.keyboard_detector = keyboard_detector
self.step = step self.step = step
lb = LineBox( lb = LineBox(
@ -82,7 +85,7 @@ class AutoDetectBase(WidgetWrap):
class AutoDetectIntro(AutoDetectBase): class AutoDetectIntro(AutoDetectBase):
def ok(self, sender): def ok(self, sender):
self.keyboard_detector.do_step(0) self.keyboard_detector.do_step("0")
def cancel(self, sender): def cancel(self, sender):
self.keyboard_detector.abort() self.keyboard_detector.abort()
@ -100,19 +103,6 @@ class AutoDetectIntro(AutoDetectBase):
]) ])
class AutoDetectFailed(AutoDetectBase):
def ok(self, sender):
self.keyboard_detector.abort()
def make_body(self):
return Pile([
Text(_("Keyboard auto detection failed, sorry")),
Text(""),
button_pile([ok_btn(label="OK", on_press=self.ok)]),
])
class AutoDetectResult(AutoDetectBase): class AutoDetectResult(AutoDetectBase):
preamble = _("""\ preamble = _("""\
@ -127,17 +117,16 @@ another layout or run the automated detection again.
""") """)
@property
def _kview(self):
return self.keyboard_detector.keyboard_view
def ok(self, sender): def ok(self, sender):
self.keyboard_detector.keyboard_view.found_layout( self._kview.found_layout(self.layout, self.variant)
self.layout, self.variant)
def make_body(self): def make_body(self):
if ':' in self.step.result: self.layout, self.variant = self._kview.lookup(
layout_code, variant_code = self.step.result.split(':') self.step.layout, self.step.variant)
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") layout_text = _("Layout")
var_text = _("Variant") var_text = _("Variant")
width = max(len(layout_text), len(var_text), 12) width = max(len(layout_text), len(var_text), 12)
@ -239,8 +228,7 @@ class Detector:
def __init__(self, kview): def __init__(self, kview):
self.keyboard_view = kview self.keyboard_view = kview
self.pc105tree = pc105.PC105Tree() self.pc105_steps = kview.keyboard_list.load_pc105()
self.pc105tree.read_steps()
self.seen_steps = [] self.seen_steps = []
def start(self): def start(self):
@ -252,9 +240,9 @@ class Detector:
self.keyboard_view.remove_overlay() self.keyboard_view.remove_overlay()
step_cls_to_view_cls = { step_cls_to_view_cls = {
pc105.StepResult: AutoDetectResult, StepResult: AutoDetectResult,
pc105.StepPressKey: AutoDetectPressKey, StepPressKey: AutoDetectPressKey,
pc105.StepKeyPresent: AutoDetectKeyPresent, StepKeyPresent: AutoDetectKeyPresent,
} }
def backup(self): def backup(self):
@ -275,14 +263,10 @@ class Detector:
self.abort() self.abort()
log.debug("moving to step %s", step_index) log.debug("moving to step %s", step_index)
try: step = self.pc105_steps[step_index]
step = self.pc105tree.steps[step_index] self.seen_steps.append(step_index)
except KeyError: log.debug("step: %s", repr(step))
self.overlay = AutoDetectFailed(self, None) self.overlay = self.step_cls_to_view_cls[type(step)](self, step)
else:
self.seen_steps.append(step_index)
log.debug("step: %s", repr(step))
self.overlay = self.step_cls_to_view_cls[type(step)](self, step)
self.overlay.start() self.overlay.start()
self.keyboard_view.show_overlay(self.overlay) self.keyboard_view.show_overlay(self.overlay)