implement keyboard selection (#276)

A new screen immediately after language.
This commit is contained in:
Michael Hudson-Doyle 2018-02-08 10:37:22 +13:00 committed by GitHub
parent 3d5bb1a280
commit 1794ed53dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 34391 additions and 7 deletions

View File

@ -1,5 +1,7 @@
Welcome: Welcome:
lang: en_US lang: en_US
Keyboard:
layout: en
Network: Network:
accept-default: yes accept-default: yes
Filesystem: Filesystem:

33600
kbdnames.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -63,6 +63,15 @@ parts:
cut -d ' ' -f 2- > users-and-groups cut -d ' ' -f 2- > users-and-groups
stage: stage:
- users-and-groups - 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: probert:
plugin: python plugin: python
build-packages: [python-setuptools, libnl-3-dev, libnl-genl-3-dev, libnl-route-3-dev] build-packages: [python-setuptools, libnl-3-dev, libnl-genl-3-dev, libnl-route-3-dev]

View File

@ -20,4 +20,5 @@ from .identity import IdentityController # NOQA
from .installpath import InstallpathController # NOQA from .installpath import InstallpathController # NOQA
from .installprogress import InstallProgressController # NOQA from .installprogress import InstallProgressController # NOQA
from .filesystem import FilesystemController # NOQA from .filesystem import FilesystemController # NOQA
from .keyboard import KeyboardController # NOQA
from .welcome import WelcomeController # NOQA from .welcome import WelcomeController # NOQA

View File

@ -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')

View File

@ -32,6 +32,7 @@ class Subiquity(Application):
controllers = [ controllers = [
"Welcome", "Welcome",
"Keyboard",
"Network", "Network",
"Filesystem", "Filesystem",
"Identity", "Identity",

View File

@ -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'])

View File

@ -25,6 +25,9 @@ class LocaleModel(object):
XXX Only represents *language* selection for now. XXX Only represents *language* selection for now.
""" """
def __init__(self, signal):
self.signal = signal
supported_languages = [ supported_languages = [
('en_US', 'English'), ('en_US', 'English'),
('ca_EN', 'Catalan'), ('ca_EN', 'Catalan'),
@ -47,6 +50,7 @@ class LocaleModel(object):
def switch_language(self, code): def switch_language(self, code):
self.selected_language = code self.selected_language = code
self.signal.emit_signal('l10n:language-selected', code)
i18n.switch_language(code) i18n.switch_language(code)
def __repr__(self): def __repr__(self):

View File

@ -21,6 +21,7 @@ from subiquitycore.models.identity import IdentityModel
from subiquitycore.models.network import NetworkModel from subiquitycore.models.network import NetworkModel
from .filesystem import FilesystemModel from .filesystem import FilesystemModel
from .keyboard import KeyboardModel
from .locale import LocaleModel from .locale import LocaleModel
@ -28,7 +29,11 @@ class SubiquityModel:
"""The overall model for subiquity.""" """The overall model for subiquity."""
def __init__(self, common): 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.network = NetworkModel()
self.filesystem = FilesystemModel(common['prober']) self.filesystem = FilesystemModel(common['prober'])
self.identity = IdentityModel() self.identity = IdentityModel()
@ -102,6 +107,12 @@ class SubiquityModel:
} }
if install_step == "install": if install_step == "install":
config.update(self.network.render()) config.update(self.network.render())
config['write_files'] = {
'etc_default_keyboard': {
'path': 'etc/default/keyboard',
'content': self.keyboard.config_content,
},
}
else: else:
config['write_files'] = self._write_files_config() config['write_files'] = self._write_files_config()

View File

@ -28,4 +28,5 @@ from .lvm import LVMVolumeGroupView # NOQA
from .identity import IdentityView # NOQA from .identity import IdentityView # NOQA
from .installpath import InstallpathView # NOQA from .installpath import InstallpathView # NOQA
from .installprogress import ProgressView # NOQA from .installprogress import ProgressView # NOQA
from .keyboard import KeyboardView
from .welcome import WelcomeView from .welcome import WelcomeView

View File

@ -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

133
subiquity/ui/views/pc105.py Normal file
View File

@ -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)

View File

@ -36,6 +36,7 @@ class BaseController(ABC):
self.pool = common['pool'] self.pool = common['pool']
self.base_model = common['base_model'] self.base_model = common['base_model']
self.all_answers = common['answers'] self.all_answers = common['answers']
self.input_filter = common['input_filter']
def register_signals(self): def register_signals(self):
"""Defines signals associated with controller from model.""" """Defines signals associated with controller from model."""

View File

@ -16,7 +16,11 @@
from concurrent import futures from concurrent import futures
import fcntl import fcntl
import logging import logging
import os
import struct
import sys import sys
import termios
import tty
import urwid import urwid
import yaml import yaml
@ -31,15 +35,23 @@ class ApplicationError(Exception):
""" Basecontroller exception """ """ Basecontroller exception """
pass pass
# The next little bit is cribbed from
# https://github.com/EvanPurkhiser/linux-vt-setcolors/blob/master/setcolors.c:
# From uapi/linux/kd.h: # From uapi/linux/kd.h:
KDGKBTYPE = 0x4B33 # get keyboard type KDGKBTYPE = 0x4B33 # get keyboard type
GIO_CMAP = 0x4B70 # gets colour palette on VGA+ GIO_CMAP = 0x4B70 # gets colour palette on VGA+
PIO_CMAP = 0x4B71 # sets colour palette on VGA+ PIO_CMAP = 0x4B71 # sets colour palette on VGA+
UO_R, UO_G, UO_B = 0xe9, 0x54, 0x20 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): 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 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: class Application:
# A concrete subclass must set project and controllers attributes, e.g.: # A concrete subclass must set project and controllers attributes, e.g.:
@ -151,6 +249,12 @@ class Application:
if not opts.dry_run: if not opts.dry_run:
open('/run/casper-no-prompt', 'w').close() 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 = { self.common = {
"ui": ui, "ui": ui,
"opts": opts, "opts": opts,
@ -159,6 +263,7 @@ class Application:
"loop": None, "loop": None,
"pool": futures.ThreadPoolExecutor(1), "pool": futures.ThreadPoolExecutor(1),
"answers": answers, "answers": answers,
"input_filter": input_filter,
} }
if opts.screens: if opts.screens:
self.controllers = [c for c in self.controllers if c in 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['loop'] = urwid.MainLoop(
self.common['ui'], palette=palette, screen=screen, 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( log.debug("Running event loop: {}".format(
self.common['loop'].event_loop)) self.common['loop'].event_loop))
self.common['base_model'] = self.model_class(self.common) self.common['base_model'] = self.model_class(self.common)
try: try:
self.common['loop'].set_alarm_in(0.05, self.next_screen) self.common['loop'].set_alarm_in(0.05, self.next_screen)
controllers_mod = __import__('%s.controllers' % self.project, None, None, ['']) controllers_mod = __import__('%s.controllers' % self.project, None, None, [''])