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/test_filesystem.py
subiquity/common/tests/test_keyboard.py
subiquity/common/tests/test_serialization.py
subiquity/common/types.py
subiquitycore/async_helpers.py
subiquitycore/contextlib38.py
@ -165,7 +166,6 @@ subiquity/ui/views/__init__.py
subiquity/ui/views/installprogress.py
subiquity/ui/views/keyboard.py
subiquity/ui/views/mirror.py
subiquity/ui/views/pc105.py
subiquity/ui/views/proxy.py
subiquity/ui/views/refresh.py
subiquity/ui/views/snaplist.py

View File

@ -4,13 +4,23 @@ from collections import defaultdict
import os
import shutil
import subprocess
import sys
from typing import Dict
from subiquity.common.serialize import Serializer
from subiquity.common.types import (
AnyStep,
KeyboardLayout,
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')
if os.path.exists(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 "
"variant!")
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
# subclasses of Step.
"""Parses the pc105.tree file into Steps"""
class Step:
def __repr__(self):
kvs = []
for k, v in self.__dict__.items():
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
from subiquity.common.types import (
StepKeyPresent,
StepPressKey,
StepResult,
)
class PC105Tree:
"""Parses the pc105.tree file into subclasses of Step"""
# This is adapted (quite heavily) from the code in ubiquity.
def __init__(self):
self.steps = {}
def _add_step_from_lines(self, lines):
step = None
step_index = -1
step_cls = None
args = None
step_index = None
for line in lines:
if line.startswith('STEP '):
step_index = int(line[5:])
step_index = line[5:].strip()
elif line.startswith('PRESS '):
# Ask the user to press a character on the keyboard.
if step is None:
step = StepPressKey()
elif not isinstance(step, StepPressKey):
if step_cls is None:
step_cls = StepPressKey
args = {
'symbols': [],
'keycodes': {},
}
elif step_cls is not StepPressKey:
raise Exception
step.symbols.append(line[6:].strip())
args['symbols'].append(line[6:].strip())
elif line.startswith('CODE '):
# Direct the evaluating code to process step ## next if the
# user has pressed a key which returned that keycode.
if not isinstance(step, StepPressKey):
if step_cls is not StepPressKey:
raise Exception
keycode = int(line[5:line.find(' ', 5)])
s = int(line[line.find(' ', 5) + 1:])
step.keycodes[keycode] = s
s = line[line.find(' ', 5) + 1:].strip()
args['keycodes'][keycode] = s
elif line.startswith('FIND '):
# Ask the user whether that character is present on their
# keyboard.
if step is None:
step = StepKeyPresent(line[5:].strip())
if step_cls is None:
step_cls = StepKeyPresent
args = {
'symbol': line[5:].strip(),
}
else:
raise Exception
elif line.startswith('FINDP '):
# Equivalent to FIND, except that the user is asked to consider
# only the primary symbols (i.e. Plain and Shift).
if step is None:
step = StepKeyPresent(line[6:].strip())
if step_cls is None:
step_cls = StepKeyPresent
args = {'symbol': line[5:].strip()}
else:
raise Exception
elif line.startswith('YES '):
# Direct the evaluating code to process step ## next if the
# user does have this key.
if not isinstance(step, StepKeyPresent):
if step_cls is not StepKeyPresent:
raise Exception
step.yes = int(line[4:].strip())
args['yes'] = line[4:].strip()
elif line.startswith('NO '):
# Direct the evaluating code to process step ## next if the
# user does not have this key.
if not isinstance(step, StepKeyPresent):
if step_cls is not StepKeyPresent:
raise Exception
step.no = int(line[3:].strip())
args['no'] = line[3:].strip()
elif line.startswith('MAP '):
# This step uniquely identifies a keymap.
if step is None:
step = StepResult(line[4:].strip())
if step_cls is None:
step_cls = StepResult
arg = line[4:].strip()
if ':' in arg:
layout, variant = arg.split(':')
else:
layout, variant = arg, ''
args = {'layout': layout, 'variant': variant}
else:
raise Exception
else:
raise Exception
if step is None or step_index == -1:
if step_cls is None or step_index is None:
raise Exception
step.check()
self.steps[step_index] = step
self.steps[step_index] = step_cls(**args)
def read_steps(self):
cur_step_lines = []

View File

@ -14,9 +14,14 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from typing import Dict
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
@ -133,3 +138,8 @@ class KeyboardList:
def _clear(self):
self.current_lang = None
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 enum
import shlex
from typing import List, Optional
from typing import Dict, List, Optional, Union
import attr
@ -92,6 +92,31 @@ class RefreshStatus:
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)
class KeyboardSetting:
layout: str

View File

@ -46,15 +46,18 @@ from subiquitycore.ui.utils import button_pile, Color, Padding, screen
from subiquitycore.view import BaseView
from subiquity.client.keyboard import for_ui, latinizable
from subiquity.common.types import KeyboardSetting
from subiquity.ui.views import pc105
from subiquity.common.types import (
KeyboardSetting,
StepKeyPresent,
StepPressKey,
StepResult,
)
log = logging.getLogger("subiquity.ui.views.keyboard")
class AutoDetectBase(WidgetWrap):
def __init__(self, keyboard_detector, step):
# step is an instance of pc105.Step
self.keyboard_detector = keyboard_detector
self.step = step
lb = LineBox(
@ -82,7 +85,7 @@ class AutoDetectBase(WidgetWrap):
class AutoDetectIntro(AutoDetectBase):
def ok(self, sender):
self.keyboard_detector.do_step(0)
self.keyboard_detector.do_step("0")
def cancel(self, sender):
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):
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):
self.keyboard_detector.keyboard_view.found_layout(
self.layout, self.variant)
self._kview.found_layout(self.layout, self.variant)
def make_body(self):
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)
self.layout, self.variant = self._kview.lookup(
self.step.layout, self.step.variant)
layout_text = _("Layout")
var_text = _("Variant")
width = max(len(layout_text), len(var_text), 12)
@ -239,8 +228,7 @@ class Detector:
def __init__(self, kview):
self.keyboard_view = kview
self.pc105tree = pc105.PC105Tree()
self.pc105tree.read_steps()
self.pc105_steps = kview.keyboard_list.load_pc105()
self.seen_steps = []
def start(self):
@ -252,9 +240,9 @@ class Detector:
self.keyboard_view.remove_overlay()
step_cls_to_view_cls = {
pc105.StepResult: AutoDetectResult,
pc105.StepPressKey: AutoDetectPressKey,
pc105.StepKeyPresent: AutoDetectKeyPresent,
StepResult: AutoDetectResult,
StepPressKey: AutoDetectPressKey,
StepKeyPresent: AutoDetectKeyPresent,
}
def backup(self):
@ -275,11 +263,7 @@ class Detector:
self.abort()
log.debug("moving to step %s", step_index)
try:
step = self.pc105tree.steps[step_index]
except KeyError:
self.overlay = AutoDetectFailed(self, None)
else:
step = self.pc105_steps[step_index]
self.seen_steps.append(step_index)
log.debug("step: %s", repr(step))
self.overlay = self.step_cls_to_view_cls[type(step)](self, step)