implement keyboard selection (#276)
A new screen immediately after language.
This commit is contained in:
parent
3d5bb1a280
commit
1794ed53dd
|
@ -1,5 +1,7 @@
|
|||
Welcome:
|
||||
lang: en_US
|
||||
Keyboard:
|
||||
layout: en
|
||||
Network:
|
||||
accept-default: yes
|
||||
Filesystem:
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -63,6 +63,15 @@ parts:
|
|||
cut -d ' ' -f 2- > users-and-groups
|
||||
stage:
|
||||
- users-and-groups
|
||||
kbdnames:
|
||||
plugin: dump
|
||||
build-packages:
|
||||
- console-setup
|
||||
- xkb-data-i18n
|
||||
prepare: |
|
||||
/usr/share/console-setup/kbdnames-maker /usr/share/console-setup/KeyboardNames.pl > kbdnames.txt
|
||||
stage:
|
||||
- kbdnames.gz
|
||||
probert:
|
||||
plugin: python
|
||||
build-packages: [python-setuptools, libnl-3-dev, libnl-genl-3-dev, libnl-route-3-dev]
|
||||
|
|
|
@ -20,4 +20,5 @@ from .identity import IdentityController # NOQA
|
|||
from .installpath import InstallpathController # NOQA
|
||||
from .installprogress import InstallProgressController # NOQA
|
||||
from .filesystem import FilesystemController # NOQA
|
||||
from .keyboard import KeyboardController # NOQA
|
||||
from .welcome import WelcomeController # NOQA
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
# Copyright 2015 Canonical, Ltd.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from subiquitycore.controller import BaseController
|
||||
|
||||
from subiquity.ui.views import KeyboardView
|
||||
|
||||
log = logging.getLogger('subiquity.controllers.keyboard')
|
||||
|
||||
class KeyboardController(BaseController):
|
||||
|
||||
signals = [
|
||||
('l10n:language-selected', 'language_selected'),
|
||||
]
|
||||
|
||||
def __init__(self, common):
|
||||
super().__init__(common)
|
||||
self.model = self.base_model.keyboard
|
||||
self.answers = self.all_answers.get("Keyboard", {})
|
||||
|
||||
def language_selected(self, code):
|
||||
log.debug("language_selected %s", code)
|
||||
if not self.model.has_language(code):
|
||||
code = code.split('_')[0]
|
||||
if not self.model.has_language(code):
|
||||
code = 'C'
|
||||
log.debug("loading launguage %s", code)
|
||||
self.model.load_language(code)
|
||||
|
||||
def default(self):
|
||||
if self.model.current_lang is None:
|
||||
self.model.load_language('C')
|
||||
title = "Keyboard configuration"
|
||||
if self.opts.run_on_serial:
|
||||
excerpt = 'Please select the layout of the keyboard directly attached to the system, if any.'
|
||||
else:
|
||||
excerpt = 'Please select your keyboard layout below, or select "Identify keyboard" to detect your layout automatically.'
|
||||
footer = _("Use UP, DOWN and ENTER keys to select your keyboard.")
|
||||
self.ui.set_header(title, excerpt)
|
||||
self.ui.set_footer(footer)
|
||||
view = KeyboardView(self.model, self, self.opts)
|
||||
self.ui.set_body(view)
|
||||
if 'layout' in self.answers:
|
||||
layout = self.answers['layout']
|
||||
variant = self.answers.get('variant')
|
||||
self.done(layout, variant)
|
||||
|
||||
def done(self, layout, variant):
|
||||
self.model.set_keyboard(layout, variant)
|
||||
self.signal.emit_signal('next-screen')
|
||||
|
||||
def cancel(self):
|
||||
self.signal.emit_signal('prev-screen')
|
|
@ -32,6 +32,7 @@ class Subiquity(Application):
|
|||
|
||||
controllers = [
|
||||
"Welcome",
|
||||
"Keyboard",
|
||||
"Network",
|
||||
"Filesystem",
|
||||
"Identity",
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
|
||||
from collections import defaultdict
|
||||
import gzip
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from subiquitycore.utils import run_command
|
||||
|
||||
log = logging.getLogger("subiquity.models.keyboard")
|
||||
|
||||
etc_default_keyboard_template = """\
|
||||
# KEYBOARD CONFIGURATION FILE
|
||||
|
||||
# Consult the keyboard(5) manual page.
|
||||
|
||||
XKBMODEL="pc105"
|
||||
XKBLAYOUT="{layout}"
|
||||
XKBVARIANT="{variant}"
|
||||
XKBOPTIONS=""
|
||||
|
||||
BACKSPACE="guess"
|
||||
"""
|
||||
|
||||
class KeyboardModel:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.layout = 'us'
|
||||
self.variant = ''
|
||||
self._kbnames_file = os.path.join(os.environ.get("SNAP", '.'), 'kbdnames.txt')
|
||||
self._clear()
|
||||
if os.path.exists(self.config_path):
|
||||
content = open(self.config_path).read()
|
||||
pat_tmpl = '(?m)^\s*%s=(.*)$'
|
||||
log.debug("%r", content)
|
||||
layout_match = re.search(pat_tmpl%("XKBLAYOUT",), content)
|
||||
if layout_match:
|
||||
log.debug("%s", layout_match)
|
||||
self.layout = layout_match.group(1).strip('"')
|
||||
variant_match = re.search(pat_tmpl%("XKBVARIANT",), content)
|
||||
if variant_match:
|
||||
log.debug("%s", variant_match)
|
||||
self.variant = variant_match.group(1).strip('"')
|
||||
if self.variant == '':
|
||||
self.variant = None
|
||||
|
||||
@property
|
||||
def config_path(self):
|
||||
return os.path.join(self.root, 'etc', 'default', 'keyboard')
|
||||
|
||||
@property
|
||||
def config_content(self):
|
||||
return etc_default_keyboard_template.format(layout=self.layout, variant=self.variant)
|
||||
|
||||
def has_language(self, code):
|
||||
self.load_language(code)
|
||||
return bool(self.layouts)
|
||||
|
||||
def load_language(self, code):
|
||||
if code == self.current_lang:
|
||||
return
|
||||
|
||||
self._clear()
|
||||
|
||||
with open(self._kbnames_file) as kbdnames:
|
||||
self._load_file(code, 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)
|
||||
return self.layouts.get(layout_code, '?'), self._variants.get(variant_code, '?')
|
||||
else:
|
||||
return self.layouts.get(code, '?'), None
|
||||
|
||||
def set_keyboard(self, layout, variant):
|
||||
path = self.config_path
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
self.layout = layout
|
||||
self.variant = variant
|
||||
with open(path, 'w') as fp:
|
||||
fp.write(self.config_content)
|
||||
if self.root == '/':
|
||||
run_command(['setupcon', '--save', '--force'])
|
|
@ -25,6 +25,9 @@ class LocaleModel(object):
|
|||
XXX Only represents *language* selection for now.
|
||||
"""
|
||||
|
||||
def __init__(self, signal):
|
||||
self.signal = signal
|
||||
|
||||
supported_languages = [
|
||||
('en_US', 'English'),
|
||||
('ca_EN', 'Catalan'),
|
||||
|
@ -47,6 +50,7 @@ class LocaleModel(object):
|
|||
|
||||
def switch_language(self, code):
|
||||
self.selected_language = code
|
||||
self.signal.emit_signal('l10n:language-selected', code)
|
||||
i18n.switch_language(code)
|
||||
|
||||
def __repr__(self):
|
||||
|
|
|
@ -21,6 +21,7 @@ from subiquitycore.models.identity import IdentityModel
|
|||
from subiquitycore.models.network import NetworkModel
|
||||
|
||||
from .filesystem import FilesystemModel
|
||||
from .keyboard import KeyboardModel
|
||||
from .locale import LocaleModel
|
||||
|
||||
|
||||
|
@ -28,7 +29,11 @@ class SubiquityModel:
|
|||
"""The overall model for subiquity."""
|
||||
|
||||
def __init__(self, common):
|
||||
self.locale = LocaleModel()
|
||||
root = '/'
|
||||
if common['opts'].dry_run:
|
||||
root = os.path.abspath(".subiquity")
|
||||
self.locale = LocaleModel(common['signal'])
|
||||
self.keyboard = KeyboardModel(root)
|
||||
self.network = NetworkModel()
|
||||
self.filesystem = FilesystemModel(common['prober'])
|
||||
self.identity = IdentityModel()
|
||||
|
@ -102,6 +107,12 @@ class SubiquityModel:
|
|||
}
|
||||
if install_step == "install":
|
||||
config.update(self.network.render())
|
||||
config['write_files'] = {
|
||||
'etc_default_keyboard': {
|
||||
'path': 'etc/default/keyboard',
|
||||
'content': self.keyboard.config_content,
|
||||
},
|
||||
}
|
||||
else:
|
||||
config['write_files'] = self._write_files_config()
|
||||
|
||||
|
|
|
@ -28,4 +28,5 @@ from .lvm import LVMVolumeGroupView # NOQA
|
|||
from .identity import IdentityView # NOQA
|
||||
from .installpath import InstallpathView # NOQA
|
||||
from .installprogress import ProgressView # NOQA
|
||||
from .keyboard import KeyboardView
|
||||
from .welcome import WelcomeView
|
||||
|
|
|
@ -0,0 +1,347 @@
|
|||
# Copyright 2018 Canonical, Ltd.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from urwid import (
|
||||
connect_signal,
|
||||
LineBox,
|
||||
Text,
|
||||
WidgetWrap,
|
||||
)
|
||||
|
||||
from subiquitycore.ui.buttons import ok_btn, other_btn
|
||||
from subiquitycore.ui.container import (
|
||||
Columns,
|
||||
ListBox,
|
||||
Pile,
|
||||
)
|
||||
from subiquitycore.ui.form import (
|
||||
Form,
|
||||
FormField,
|
||||
)
|
||||
from subiquitycore.ui.selector import Option, Selector
|
||||
from subiquitycore.ui.utils import button_pile, Color, Padding
|
||||
from subiquitycore.view import BaseView
|
||||
|
||||
from subiquity.ui.views import pc105
|
||||
|
||||
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(self.make_body(), _("Keyboard auto-detection"))
|
||||
super().__init__(lb)
|
||||
def start(self):
|
||||
pass
|
||||
def stop(self):
|
||||
pass
|
||||
def keypress(self, size, key):
|
||||
if key == 'esc':
|
||||
self.keyboard_detector.backup()
|
||||
else:
|
||||
return super().keypress(size, key)
|
||||
|
||||
|
||||
class AutoDetectIntro(AutoDetectBase):
|
||||
|
||||
def ok(self, sender):
|
||||
self.keyboard_detector.do_step(0)
|
||||
|
||||
def cancel(self, sender):
|
||||
self.keyboard_detector.abort()
|
||||
|
||||
def make_body(self):
|
||||
return Pile([
|
||||
Text(_("Keyboard detection starting. You will be asked a series of questions about your keyboard. Press escape at any time to go back to the previous screen.")),
|
||||
Text(""),
|
||||
button_pile([
|
||||
ok_btn(label=_("OK"), on_press=self.ok),
|
||||
ok_btn(label=_("Cancel"), on_press=self.cancel),
|
||||
]),
|
||||
])
|
||||
|
||||
|
||||
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 = _("""\
|
||||
Keyboard auto detection completed.
|
||||
|
||||
Your keyboard was detected as:
|
||||
""")
|
||||
postamble = _("""\
|
||||
|
||||
If this is correct, select Done on the next screen. If not you can select \
|
||||
another layout or run the automated detection again.
|
||||
|
||||
""")
|
||||
|
||||
def ok(self, sender):
|
||||
self.keyboard_detector.keyboard_view.found_layout(self.step.result)
|
||||
|
||||
def make_body(self):
|
||||
model = self.keyboard_detector.keyboard_view.model
|
||||
layout, variant = model.lookup(self.step.result)
|
||||
var_desc = []
|
||||
if variant is not None:
|
||||
var_desc = [Text(_(" Variant: ") + variant)]
|
||||
return Pile([
|
||||
Text(self.preamble),
|
||||
Text(_(" Layout: ") + layout),
|
||||
] + var_desc + [
|
||||
Text(self.postamble),
|
||||
button_pile([ok_btn(label=_("OK"), on_press=self.ok)]),
|
||||
])
|
||||
|
||||
|
||||
class AutoDetectPressKey(AutoDetectBase):
|
||||
# This is the tricky case. We need access to the "keycodes" not
|
||||
# the characters that the current keyboard set up maps the
|
||||
# keycodes to. The heavy lifting is done by the InputFilter class
|
||||
# in subiquitycore.core.
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def make_body(self):
|
||||
self.error_text = Text("", align="center")
|
||||
return Pile([
|
||||
Text(_("Please press one of the following keys:")),
|
||||
Text(""),
|
||||
Columns([Text(s, align="center") for s in self.step.symbols], dividechars=1),
|
||||
Text(""),
|
||||
Color.info_error(self.error_text),
|
||||
])
|
||||
|
||||
@property
|
||||
def input_filter(self):
|
||||
return self.keyboard_detector.keyboard_view.controller.input_filter
|
||||
|
||||
def start(self):
|
||||
self.input_filter.enter_keycodes_mode()
|
||||
|
||||
def stop(self):
|
||||
self.input_filter.exit_keycodes_mode()
|
||||
|
||||
def keypress(self, size, key):
|
||||
log.debug('keypress %r %r', size, key)
|
||||
if key.startswith('release '):
|
||||
# Escape is key 1 on all keyboards and all layouts except
|
||||
# amigas and very old Macs so this seems safe enough.
|
||||
if key == 'release 1':
|
||||
return super().keypress(size, 'esc')
|
||||
else:
|
||||
return
|
||||
elif key.startswith('press '):
|
||||
code = int(key[len('press '):])
|
||||
if code not in self.step.keycodes:
|
||||
self.error_text.set_text(_("Input was not recognized, try again"))
|
||||
return
|
||||
v = self.step.keycodes[code]
|
||||
else:
|
||||
# If we're not on a linux tty, the filtering won't have
|
||||
# happened and so there's no way to get the keycodes. Do
|
||||
# something literally random instead.
|
||||
import random
|
||||
v = random.choice(list(self.step.keycodes.values()))
|
||||
self.keyboard_detector.do_step(v)
|
||||
|
||||
|
||||
class AutoDetectKeyPresent(AutoDetectBase):
|
||||
|
||||
def yes(self, sender):
|
||||
self.keyboard_detector.do_step(self.step.yes)
|
||||
|
||||
def no(self, sender):
|
||||
self.keyboard_detector.do_step(self.step.no)
|
||||
|
||||
def make_body(self):
|
||||
return Pile([
|
||||
Text(_("Is the following key present on your keyboard?")),
|
||||
Text(""),
|
||||
Text(self.step.symbol, align="center"),
|
||||
Text(""),
|
||||
button_pile([
|
||||
ok_btn(label=_("Yes"), on_press=self.yes),
|
||||
other_btn(label=_("No"), on_press=self.no),
|
||||
]),
|
||||
])
|
||||
|
||||
|
||||
class Detector:
|
||||
# Encapsulates the state of the autodetection process.
|
||||
|
||||
def __init__(self, kview):
|
||||
self.keyboard_view = kview
|
||||
self.pc105tree = pc105.PC105Tree()
|
||||
self.pc105tree.read_steps()
|
||||
self.seen_steps = []
|
||||
|
||||
def start(self):
|
||||
o = AutoDetectIntro(self, None)
|
||||
self.keyboard_view.show_overlay(o)
|
||||
|
||||
def abort(self):
|
||||
overlay = self.keyboard_view._w.top_w
|
||||
overlay.stop()
|
||||
self.keyboard_view.remove_overlay()
|
||||
|
||||
step_cls_to_view_cls = {
|
||||
pc105.StepResult: AutoDetectResult,
|
||||
pc105.StepPressKey: AutoDetectPressKey,
|
||||
pc105.StepKeyPresent: AutoDetectKeyPresent,
|
||||
}
|
||||
|
||||
def backup(self):
|
||||
if len(self.seen_steps) == 0:
|
||||
self.seen_steps = []
|
||||
self.abort()
|
||||
return
|
||||
if len(self.seen_steps) == 1:
|
||||
self.seen_steps = []
|
||||
self.abort()
|
||||
self.start()
|
||||
return
|
||||
self.seen_steps.pop()
|
||||
step_index = self.seen_steps.pop()
|
||||
self.do_step(step_index)
|
||||
|
||||
def do_step(self, step_index):
|
||||
self.abort()
|
||||
|
||||
log.debug("moving to step %s", step_index)
|
||||
try:
|
||||
step = self.pc105tree.steps[step_index]
|
||||
except KeyError:
|
||||
view = AutoDetectFailed(self, None)
|
||||
else:
|
||||
self.seen_steps.append(step_index)
|
||||
log.debug("step: %s", repr(step))
|
||||
view = self.step_cls_to_view_cls[type(step)](self, step)
|
||||
|
||||
view.start()
|
||||
self.keyboard_view.show_overlay(view)
|
||||
|
||||
|
||||
class ChoiceField(FormField):
|
||||
|
||||
def __init__(self, caption=None, help=None, choices=[]):
|
||||
super().__init__(caption, help)
|
||||
self.choices = choices
|
||||
|
||||
def _make_widget(self, form):
|
||||
return Selector(self.choices)
|
||||
|
||||
class KeyboardForm(Form):
|
||||
|
||||
cancel_label = _("Back")
|
||||
|
||||
layout = ChoiceField(_("Layout:"), choices=["dummy"])
|
||||
variant = ChoiceField(_("Variant:"), choices=["dummy"])
|
||||
|
||||
|
||||
class KeyboardView(BaseView):
|
||||
|
||||
def __init__(self, model, controller, opts):
|
||||
self.model = model
|
||||
self.controller = controller
|
||||
self.opts = opts
|
||||
|
||||
self.form = KeyboardForm()
|
||||
opts = []
|
||||
for layout, desc in model.layouts.items():
|
||||
opts.append(Option((desc, True, layout)))
|
||||
opts.sort(key=lambda o:o.label)
|
||||
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
|
||||
self.form.layout.widget.value = model.layout
|
||||
self.form.variant.widget.value = model.variant
|
||||
|
||||
self._rows = self.form.as_rows(self)
|
||||
|
||||
lb_contents = [self._rows]
|
||||
if not self.opts.run_on_serial:
|
||||
lb_contents.extend([
|
||||
Text(""),
|
||||
button_pile([
|
||||
other_btn(label=_("Identify keyboard"), on_press=self.detect)]),
|
||||
])
|
||||
lb = ListBox(lb_contents)
|
||||
pile = Pile([
|
||||
('pack', Text("")),
|
||||
Padding.center_90(lb),
|
||||
('pack', Pile([
|
||||
Text(""),
|
||||
self.form.buttons,
|
||||
Text(""),
|
||||
])),
|
||||
])
|
||||
lb._select_last_selectable()
|
||||
pile.focus_position = 2
|
||||
super().__init__(pile)
|
||||
|
||||
def detect(self, sender):
|
||||
detector = Detector(self)
|
||||
detector.start()
|
||||
|
||||
def found_layout(self, result):
|
||||
self.remove_overlay()
|
||||
log.debug("found_layout %s", result)
|
||||
if ':' in result:
|
||||
layout, variant = result.split(':')
|
||||
else:
|
||||
layout, variant = result, None
|
||||
self.form.layout.widget.value = layout
|
||||
self.form.variant.widget.value = variant
|
||||
self._w.focus_position = 2
|
||||
|
||||
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
|
||||
self.controller.done(layout, variant)
|
||||
|
||||
def cancel(self, result=None):
|
||||
self.controller.cancel()
|
||||
|
||||
def select_layout(self, sender, layout):
|
||||
log.debug("%s", layout)
|
||||
opts = []
|
||||
for variant, variant_desc in self.model.variants[layout].items():
|
||||
opts.append(Option((variant_desc, True, variant)))
|
||||
opts.sort(key=lambda o:o.label)
|
||||
opts.insert(0, Option(("default", True, None)))
|
||||
self.form.variant.widget._options = opts
|
||||
self.form.variant.widget.index = 0
|
||||
self.form.variant.enabled = len(opts) > 1
|
|
@ -0,0 +1,133 @@
|
|||
# Copyright 2018 Canonical, Ltd.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# The keyboard autodetection process is driven by the data in
|
||||
# /usr/share/console-setup/pc105.tree. This code parses that data into
|
||||
# subclasses of Step.
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
for line in lines:
|
||||
if line.startswith('STEP '):
|
||||
step_index = int(line[5:])
|
||||
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):
|
||||
raise Exception
|
||||
step.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):
|
||||
raise Exception
|
||||
keycode = int(line[5:line.find(' ', 5)])
|
||||
s = int(line[line.find(' ', 5) + 1:])
|
||||
step.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())
|
||||
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())
|
||||
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):
|
||||
raise Exception
|
||||
step.yes = int(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):
|
||||
raise Exception
|
||||
step.no = int(line[3:].strip())
|
||||
elif line.startswith('MAP '):
|
||||
# This step uniquely identifies a keymap.
|
||||
if step is None:
|
||||
step = StepResult(line[4:].strip())
|
||||
else:
|
||||
raise Exception
|
||||
else:
|
||||
raise Exception
|
||||
if step is None or step_index == -1:
|
||||
raise Exception
|
||||
step.check()
|
||||
self.steps[step_index] = step
|
||||
|
||||
def read_steps(self):
|
||||
cur_step_lines = []
|
||||
with open('/usr/share/console-setup/pc105.tree') as fp:
|
||||
for line in fp:
|
||||
if line.startswith('STEP '):
|
||||
if cur_step_lines:
|
||||
self._add_step_from_lines(cur_step_lines)
|
||||
cur_step_lines = [line]
|
||||
else:
|
||||
cur_step_lines.append(line)
|
||||
if cur_step_lines:
|
||||
self._add_step_from_lines(cur_step_lines)
|
|
@ -36,6 +36,7 @@ class BaseController(ABC):
|
|||
self.pool = common['pool']
|
||||
self.base_model = common['base_model']
|
||||
self.all_answers = common['answers']
|
||||
self.input_filter = common['input_filter']
|
||||
|
||||
def register_signals(self):
|
||||
"""Defines signals associated with controller from model."""
|
||||
|
|
|
@ -16,7 +16,11 @@
|
|||
from concurrent import futures
|
||||
import fcntl
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import termios
|
||||
import tty
|
||||
|
||||
import urwid
|
||||
import yaml
|
||||
|
@ -31,15 +35,23 @@ class ApplicationError(Exception):
|
|||
""" Basecontroller exception """
|
||||
pass
|
||||
|
||||
# The next little bit is cribbed from
|
||||
# https://github.com/EvanPurkhiser/linux-vt-setcolors/blob/master/setcolors.c:
|
||||
|
||||
# From uapi/linux/kd.h:
|
||||
KDGKBTYPE = 0x4B33 # get keyboard type
|
||||
|
||||
GIO_CMAP = 0x4B70 # gets colour palette on VGA+
|
||||
PIO_CMAP = 0x4B71 # sets colour palette on VGA+
|
||||
UO_R, UO_G, UO_B = 0xe9, 0x54, 0x20
|
||||
|
||||
# /usr/include/linux/kd.h
|
||||
K_RAW = 0x00
|
||||
K_XLATE = 0x01
|
||||
K_MEDIUMRAW = 0x02
|
||||
K_UNICODE = 0x03
|
||||
K_OFF = 0x04
|
||||
|
||||
KDGKBMODE = 0x4B44 # gets current keyboard mode
|
||||
KDSKBMODE = 0x4B45 # sets current keyboard mode
|
||||
|
||||
|
||||
class ISO_8613_3_Screen(urwid.raw_display.Screen):
|
||||
|
||||
|
@ -116,6 +128,92 @@ def setup_screen(colors, styles):
|
|||
return ISO_8613_3_Screen(_urwid_name_to_rgb), urwid_palette
|
||||
|
||||
|
||||
|
||||
class KeyCodesFilter:
|
||||
"""input_filter that can pass (medium) raw keycodes to the application
|
||||
|
||||
See http://lct.sourceforge.net/lct/x60.html for terminology.
|
||||
|
||||
Call enter_keycodes_mode()/exit_keycodes_mode() to switch into and
|
||||
out of keycodes mode. In keycodes mode, the only events passed to
|
||||
the application are "press $N" / "release $N" where $N is the
|
||||
keycode the user pressed or released.
|
||||
|
||||
Much of this is cribbed from the source of the "showkeys" utility.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._fd = os.open("/proc/self/fd/0", os.O_RDWR)
|
||||
self.filtering = False
|
||||
|
||||
def enter_keycodes_mode(self):
|
||||
log.debug("enter_keycodes_mode")
|
||||
self.filtering = True
|
||||
# Read the old keyboard mode (it will proably always be K_UNICODE but well).
|
||||
o = bytearray(4)
|
||||
fcntl.ioctl(self._fd, KDGKBMODE, o)
|
||||
self._old_mode = struct.unpack('i', o)[0]
|
||||
# Make some changes to the terminal settings.
|
||||
# If you don't do this, sometimes writes to the terminal hang (and no,
|
||||
# I don't know exactly why).
|
||||
self._old_settings = termios.tcgetattr(self._fd)
|
||||
new_settings = termios.tcgetattr(self._fd)
|
||||
new_settings[tty.IFLAG] = 0
|
||||
new_settings[tty.LFLAG] = new_settings[tty.LFLAG] & ~(termios.ECHO | termios.ICANON | termios.ISIG)
|
||||
new_settings[tty.CC][termios.VMIN] = 0
|
||||
new_settings[tty.CC][termios.VTIME] = 0
|
||||
termios.tcsetattr(self._fd, termios.TCSAFLUSH, new_settings)
|
||||
# Finally, set the keyboard mode to K_MEDIUMRAW, which causes
|
||||
# the keyboard driver in the kernel to pass us keycodes.
|
||||
log.debug("old mode was %s, setting mode to %s", self._old_mode, K_MEDIUMRAW)
|
||||
fcntl.ioctl(self._fd, KDSKBMODE, K_MEDIUMRAW)
|
||||
|
||||
def exit_keycodes_mode(self):
|
||||
log.debug("exit_keycodes_mode")
|
||||
self.filtering = False
|
||||
log.debug("setting mode back to %s", self._old_mode)
|
||||
fcntl.ioctl(self._fd, KDSKBMODE, self._old_mode)
|
||||
termios.tcsetattr(self._fd, termios.TCSANOW, self._old_settings)
|
||||
|
||||
def filter(self, keys, codes):
|
||||
# Luckily urwid passes us the raw results from read() we can
|
||||
# turn into keycodes.
|
||||
if self.filtering:
|
||||
i = 0
|
||||
r = []
|
||||
n = len(codes)
|
||||
while i < len(codes):
|
||||
# This is straight from showkeys.c.
|
||||
if codes[i] & 0x80:
|
||||
p = 'release '
|
||||
else:
|
||||
p = 'press '
|
||||
if i + 2 < n and (codes[i] & 0x7f) == 0 and (codes[i + 1] & 0x80) != 0 and (codes[i + 2] & 0x80) != 0:
|
||||
kc = ((codes[i + 1] & 0x7f) << 7) | (codes[i + 2] & 0x7f)
|
||||
i += 3
|
||||
else:
|
||||
kc = codes[i] & 0x7f
|
||||
i += 1
|
||||
r.append(p + str(kc))
|
||||
return r
|
||||
else:
|
||||
return keys
|
||||
|
||||
|
||||
class DummyKeycodesFilter:
|
||||
# A dummy implementation of the same interface as KeyCodesFilter
|
||||
# we can use when not running in a linux tty.
|
||||
|
||||
def enter_keycodes_mode(self):
|
||||
pass
|
||||
|
||||
def exit_keycodes_mode(self):
|
||||
pass
|
||||
|
||||
def filter(self, keys, codes):
|
||||
return keys
|
||||
|
||||
|
||||
class Application:
|
||||
|
||||
# A concrete subclass must set project and controllers attributes, e.g.:
|
||||
|
@ -151,6 +249,12 @@ class Application:
|
|||
if not opts.dry_run:
|
||||
open('/run/casper-no-prompt', 'w').close()
|
||||
|
||||
if is_linux_tty():
|
||||
log.debug("is_linux_tty")
|
||||
input_filter = KeyCodesFilter()
|
||||
else:
|
||||
input_filter = DummyKeycodesFilter()
|
||||
|
||||
self.common = {
|
||||
"ui": ui,
|
||||
"opts": opts,
|
||||
|
@ -159,6 +263,7 @@ class Application:
|
|||
"loop": None,
|
||||
"pool": futures.ThreadPoolExecutor(1),
|
||||
"answers": answers,
|
||||
"input_filter": input_filter,
|
||||
}
|
||||
if opts.screens:
|
||||
self.controllers = [c for c in self.controllers if c in opts.screens]
|
||||
|
@ -226,12 +331,11 @@ class Application:
|
|||
|
||||
self.common['loop'] = urwid.MainLoop(
|
||||
self.common['ui'], palette=palette, screen=screen,
|
||||
handle_mouse=False, pop_ups=True)
|
||||
handle_mouse=False, pop_ups=True, input_filter=self.common['input_filter'].filter)
|
||||
|
||||
log.debug("Running event loop: {}".format(
|
||||
self.common['loop'].event_loop))
|
||||
|
||||
self.common['base_model'] = self.model_class(self.common)
|
||||
|
||||
try:
|
||||
self.common['loop'].set_alarm_in(0.05, self.next_screen)
|
||||
controllers_mod = __import__('%s.controllers' % self.project, None, None, [''])
|
||||
|
|
Loading…
Reference in New Issue