From 210cdcb51bc4de27eee9717daa919dc8222f9fbf Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Mon, 27 Jul 2020 23:20:41 +1200 Subject: [PATCH] Move the code for setting up the screen to its own file Looking at this code thinking about the coming client / server split made me realise that we could start by at least moving this functionality to a more encapsulated place. --- console_conf/core.py | 4 - subiquity/controllers/welcome.py | 4 +- subiquity/core.py | 2 - subiquity/ui/views/welcome.py | 4 +- subiquitycore/core.py | 164 ++----------------------------- subiquitycore/palette.py | 51 ++++++++-- subiquitycore/screen.py | 151 ++++++++++++++++++++++++++++ 7 files changed, 208 insertions(+), 172 deletions(-) create mode 100644 subiquitycore/screen.py diff --git a/console_conf/core.py b/console_conf/core.py index 9540ef5f..fb621ff6 100644 --- a/console_conf/core.py +++ b/console_conf/core.py @@ -25,8 +25,6 @@ log = logging.getLogger("console_conf.core") class ConsoleConf(Application): - from subiquitycore.palette import COLORS, STYLES, STYLES_MONO - project = "console_conf" make_model = ConsoleConfModel @@ -40,8 +38,6 @@ class ConsoleConf(Application): class RecoveryChooser(Application): - from subiquitycore.palette import COLORS, STYLES, STYLES_MONO - project = "console_conf" controllers = [ diff --git a/subiquity/controllers/welcome.py b/subiquity/controllers/welcome.py index eac965be..54fcd627 100644 --- a/subiquity/controllers/welcome.py +++ b/subiquity/controllers/welcome.py @@ -16,6 +16,8 @@ import logging import os +from subiquitycore.screen import is_linux_tty + from subiquity.controller import SubiquityController from subiquity.ui.views import WelcomeView @@ -39,7 +41,7 @@ class WelcomeController(SubiquityController): lang = os.environ.get("LANG") if lang is not None and lang.endswith(".UTF-8"): lang = lang.rsplit('.', 1)[0] - for code, name in self.model.get_languages(self.app.is_linux_tty): + for code, name in self.model.get_languages(is_linux_tty()): if code == lang: self.model.switch_language(code) break diff --git a/subiquity/core.py b/subiquity/core.py index 65b086c2..fdd65478 100644 --- a/subiquity/core.py +++ b/subiquity/core.py @@ -85,8 +85,6 @@ class Subiquity(Application): 'additionalProperties': True, } - from subiquitycore.palette import COLORS, STYLES, STYLES_MONO - project = "subiquity" def make_model(self): diff --git a/subiquity/ui/views/welcome.py b/subiquity/ui/views/welcome.py index a6c6e1b0..207a881f 100644 --- a/subiquity/ui/views/welcome.py +++ b/subiquity/ui/views/welcome.py @@ -25,6 +25,7 @@ from urwid import Text from subiquitycore.ui.buttons import forward_btn, other_btn from subiquitycore.ui.container import ListBox from subiquitycore.ui.utils import button_pile, rewrap, screen +from subiquitycore.screen import is_linux_tty from subiquitycore.view import BaseView from subiquity.ui.views.help import ( @@ -64,7 +65,6 @@ class WelcomeView(BaseView): def __init__(self, model, controller): self.model = model self.controller = controller - self.is_linux_tty = controller.app.is_linux_tty if controller.app.opts.run_on_serial and not controller.app.rich_mode: s = self.make_serial_choices() self.title = "Welcome!" @@ -75,7 +75,7 @@ class WelcomeView(BaseView): def make_language_choices(self): btns = [] current_index = None - langs = self.model.get_languages(self.is_linux_tty) + langs = self.model.get_languages(is_linux_tty()) cur = self.model.selected_language log.debug("_build_model_inputs selected_language=%s", cur) if cur in ["C", None]: diff --git a/subiquitycore/core.py b/subiquitycore/core.py index e5a6aaa4..91768eba 100644 --- a/subiquitycore/core.py +++ b/subiquitycore/core.py @@ -20,7 +20,6 @@ import logging import os import struct import sys -import tty import urwid import yaml @@ -32,21 +31,16 @@ from subiquitycore.context import ( from subiquitycore.controller import ( Skip, ) -from subiquitycore.signals import Signal +from subiquitycore.palette import PALETTE_COLOR, PALETTE_MONO from subiquitycore.prober import Prober +from subiquitycore.screen import is_linux_tty, make_screen +from subiquitycore.signals import Signal from subiquitycore.ui.frame import SubiquityCoreUI from subiquitycore.utils import arun_command log = logging.getLogger('subiquitycore.core') -# 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 @@ -58,86 +52,6 @@ KDGKBMODE = 0x4B44 # gets current keyboard mode KDSKBMODE = 0x4B45 # sets current keyboard mode -class TwentyFourBitScreen(urwid.raw_display.Screen): - - def __init__(self, _urwid_name_to_rgb, **kwargs): - self._urwid_name_to_rgb = _urwid_name_to_rgb - super().__init__(**kwargs) - - def _cc(self, color): - """Return the "SGR" parameter for selecting color. - - See https://en.wikipedia.org/wiki/ANSI_escape_code#SGR for an - explanation. We use the basic codes for black/white/default for - maximum compatibility; they are the only colors used when the - mono palette is selected. - """ - if color == 'white': - return '7' - elif color == 'black': - return '0' - elif color == 'default': - return '9' - else: - # This is almost but not quite a ISO 8613-3 code -- that - # would use colons to separate the rgb values instead. But - # it's what xterm, and hence everything else, supports. - return '8;2;{};{};{}'.format(*self._urwid_name_to_rgb[color]) - - def _attrspec_to_escape(self, a): - return '\x1b[0;3{};4{}m'.format( - self._cc(a.foreground), - self._cc(a.background)) - - -def is_linux_tty(): - try: - r = fcntl.ioctl(sys.stdout.fileno(), KDGKBTYPE, ' ') - except IOError as e: - log.debug("KDGKBTYPE failed %r", e) - return False - log.debug("KDGKBTYPE returned %r, is_linux_tty %s", r, r == b'\x02') - return r == b'\x02' - - -urwid_8_names = ( - 'black', - 'dark red', - 'dark green', - 'brown', - 'dark blue', - 'dark magenta', - 'dark cyan', - 'light gray', -) - - -def make_palette(colors, styles): - """Return a palette to be passed to MainLoop. - - colors is a list of exactly 8 tuples (name, (r, g, b)) - - styles is a list of tuples (stylename, fg_color, bg_color) where - fg_color and bg_color are defined in 'colors' - """ - # The part that makes this "fun" is that urwid insists on referring - # to the basic colors by their "standard" names but we overwrite - # these colors to mean different things. So we convert styles into - # an urwid palette by mapping the names in colors to the standard - # name. - if len(colors) != 8: - raise Exception( - "make_palette must be passed a list of exactly 8 colors") - urwid_name = dict(zip([c[0] for c in colors], urwid_8_names)) - - urwid_palette = [] - for name, fg, bg in styles: - urwid_fg, urwid_bg = urwid_name[fg], urwid_name[bg] - urwid_palette.append((name, urwid_fg, urwid_bg)) - - return urwid_palette - - def extend_dec_special_charmap(): urwid.escape.DEC_SPECIAL_CHARMAP.update({ ord('\N{BLACK RIGHT-POINTING SMALL TRIANGLE}'): '>', @@ -341,11 +255,8 @@ class Application: # Set rich_mode to the opposite of what we want, so we can # call toggle_rich to get the right things set up. self.rich_mode = opts.run_on_serial - self.color_palette = make_palette(self.COLORS, self.STYLES) - self.is_linux_tty = is_linux_tty() - - if self.is_linux_tty: + if is_linux_tty(): self.input_filter = KeyCodesFilter() else: self.input_filter = DummyKeycodesFilter() @@ -369,29 +280,10 @@ class Application: **kw): screen = self.urwid_loop.screen - # Calling screen.stop() sends the INPUT_DESCRIPTORS_CHANGED - # signal. This calls _reset_input_descriptors() which calls - # unhook_event_loop / hook_event_loop on the screen. But this all - # happens before _started is set to False on the screen and so this - # does not actually do anything -- we end up attempting to read from - # stdin while in a background process group, something that gets the - # kernel upset at us. - # - # The cleanest fix seems to be to just send the signal again once - # stop() has returned which, now that screen._started is False, - # correctly stops listening from stdin. - # - # There is an exactly analagous problem with screen.start() except - # there the symptom is that we are running in the foreground but not - # listening to stdin! The fix is the same. - async def _run(): await arun_command( cmd, stdin=None, stdout=None, stderr=None, **kw) screen.start() - urwid.emit_signal( - screen, urwid.display_common.INPUT_DESCRIPTORS_CHANGED) - self.setraw() if after_hook is not None: after_hook() @@ -552,11 +444,11 @@ class Application: def toggle_rich(self): if self.rich_mode: urwid.util.set_encoding('ascii') - new_palette = self.STYLES_MONO + new_palette = PALETTE_MONO self.rich_mode = False else: urwid.util.set_encoding('utf-8') - new_palette = self.color_palette + new_palette = PALETTE_COLOR self.rich_mode = True urwid.CanvasCache.clear() self.urwid_loop.screen.register_palette(new_palette) @@ -598,53 +490,14 @@ class Application: controller.configured() return controller_index - def setraw(self): - fd = self.urwid_loop.screen._term_input_file.fileno() - if os.isatty(fd): - tty.setraw(fd) - def make_screen(self, inputf=None, outputf=None): - """Return a screen to be passed to MainLoop. - - colors is a list of exactly 8 tuples (name, (r, g, b)), the same as - passed to make_palette. - """ - # On the linux console, we overwrite the first 8 colors to be those - # defined by colors. Otherwise, we return a screen that uses ISO - # 8613-3ish codes to display the colors. - if inputf is None: - inputf = sys.stdin - if outputf is None: - outputf = sys.stdout - - if len(self.COLORS) != 8: - raise Exception( - "make_screen must be passed a list of exactly 8 colors") - if self.is_linux_tty: - # Perhaps we ought to return a screen subclass that does this - # ioctl-ing in .start() and undoes it in .stop() but well. - curpal = bytearray(16*3) - fcntl.ioctl(sys.stdout.fileno(), GIO_CMAP, curpal) - for i in range(8): - for j in range(3): - curpal[i*3+j] = self.COLORS[i][1][j] - fcntl.ioctl(sys.stdout.fileno(), PIO_CMAP, curpal) - return urwid.raw_display.Screen(input=inputf, output=outputf) - elif self.opts.ascii: - return urwid.raw_display.Screen(input=inputf, output=outputf) - else: - _urwid_name_to_rgb = {} - for i, n in enumerate(urwid_8_names): - _urwid_name_to_rgb[n] = self.COLORS[i][1] - return TwentyFourBitScreen(_urwid_name_to_rgb, - input=inputf, output=outputf) + return make_screen(self.opts.ascii, inputf, outputf) def run(self, input=None, output=None): log.debug("Application.run") - screen = self.make_screen(input, output) self.urwid_loop = urwid.MainLoop( - self.ui, palette=self.color_palette, screen=screen, + self.ui, screen=self.make_screen(input, output), handle_mouse=False, pop_ups=True, input_filter=self.input_filter.filter, unhandled_input=self.unhandled_input, @@ -666,7 +519,6 @@ class Application: if self.updated: initial_controller_index = self.load_serialized_state() - self.aio_loop.call_soon(self.setraw) self.aio_loop.call_soon( self.select_initial_screen, initial_controller_index) self._connect_base_signals() diff --git a/subiquitycore/palette.py b/subiquitycore/palette.py index 4759d822..c7e1be4c 100644 --- a/subiquitycore/palette.py +++ b/subiquitycore/palette.py @@ -1,4 +1,4 @@ -# Copyright 2015 Canonical, Ltd. +# Copyright 2020 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 @@ -13,13 +13,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -""" Palette definitions """ - COLORS = [ # black ("bg", (0x11, 0x11, 0x11)), - # dark read + # dark red ("danger", (0xff, 0x00, 0x00)), # dark green ("good", (0x0e, 0x84, 0x20)), @@ -35,7 +33,7 @@ COLORS = [ ("fg", (0xff, 0xff, 0xff)), ] -STYLES = [ +PALETTE_COLOR = [ ('frame_header_fringe', 'orange', 'bg'), ('frame_header', 'fg', 'orange'), ('body', 'fg', 'bg'), @@ -71,8 +69,7 @@ STYLES = [ ('verified focus', 'good', 'gray'), ] - -STYLES_MONO = [ +PALETTE_MONO = [ ('frame_header_fringe', 'white', 'black'), ('frame_header', 'black', 'white'), ('body', 'white', 'black'), @@ -102,3 +99,43 @@ STYLES_MONO = [ ('scrollbar_fg', 'white', 'black'), ('scrollbar_bg', 'white', 'black'), ] + +urwid_8_names = ( + 'black', + 'dark red', + 'dark green', + 'brown', + 'dark blue', + 'dark magenta', + 'dark cyan', + 'light gray', +) + + +def _urwidize_palette(colors, styles): + """Return a palette to be passed to MainLoop. + + colors is a list of exactly 8 tuples (name, (r, g, b)) + + styles is a list of tuples (stylename, fg_color, bg_color) where + fg_color and bg_color are defined in 'colors' + """ + # The part that makes this "fun" is that urwid insists on referring + # to the basic colors by their "standard" names but we overwrite + # these colors to mean different things. So we convert styles into + # an urwid palette by mapping the names in colors to the standard + # name. + if len(colors) != 8: + raise Exception( + "make_palette must be passed a list of exactly 8 colors") + urwid_name = dict(zip([c[0] for c in colors], urwid_8_names)) + + urwid_palette = [] + for name, fg, bg in styles: + urwid_fg, urwid_bg = urwid_name[fg], urwid_name[bg] + urwid_palette.append((name, urwid_fg, urwid_bg)) + + return urwid_palette + + +PALETTE_COLOR = _urwidize_palette(COLORS, PALETTE_COLOR) diff --git a/subiquitycore/screen.py b/subiquitycore/screen.py new file mode 100644 index 00000000..43672cf2 --- /dev/null +++ b/subiquitycore/screen.py @@ -0,0 +1,151 @@ +# Copyright 2020 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 . + +import fcntl +import logging +import os +import sys +import tty + +import urwid + +from subiquitycore.palette import COLORS, urwid_8_names + +log = logging.getLogger('subiquitycore.screen') + + +# 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 + + +class SubiquityScreen(urwid.raw_display.Screen): + + # This class fixes a bug in urwid's screen: + # + # Calling screen.stop() sends the INPUT_DESCRIPTORS_CHANGED signal. This + # calls _reset_input_descriptors() which calls unhook_event_loop / + # hook_event_loop on the screen. But this all happens before _started is + # set to False on the screen and so this does not actually do anything. If + # we call stop and then, for example, run bash, we end up attempting to + # read from stdin while in a background process group and that gets the + # kernel upset at us. + # + # The cleanest fix seems to be to just send the signal again once stop() + # has returned which, now that screen._started is False, correctly stops + # listening from stdin. + # + # There is an exactly analagous problem with screen.start() except there + # the symptom is that we are running in the foreground but not listening to + # stdin! The fix is the same. + + def start(self): + super().start() + urwid.emit_signal(self, urwid.display_common.INPUT_DESCRIPTORS_CHANGED) + # We run the terminal in raw, not cbreak mode. + fd = self._term_input_file.fileno() + if os.isatty(fd): + tty.setraw(fd) + + def stop(self): + super().stop() + urwid.emit_signal(self, urwid.display_common.INPUT_DESCRIPTORS_CHANGED) + + +class LinuxScreen(SubiquityScreen): + + def __init__(self, colors, **kwargs): + self._colors = colors + super().__init__(**kwargs) + + def start(self): + self.curpal = bytearray(16*3) + fcntl.ioctl(sys.stdout.fileno(), GIO_CMAP, self.curpal) + newpal = self.curpal.copy() + for i in range(8): + for j in range(3): + newpal[i*3+j] = self._colors[i][1][j] + fcntl.ioctl(self._term_input_file.fileno(), PIO_CMAP, newpal) + super().start() + + def stop(self): + fcntl.ioctl(self._term_input_file.fileno(), PIO_CMAP, self.curpal) + super().stop() + + +class TwentyFourBitScreen(SubiquityScreen): + + def __init__(self, colors, **kwargs): + self._urwid_name_to_rgb = { + n: colors[i][1] for i, n in enumerate(urwid_8_names)} + super().__init__(**kwargs) + + def _cc(self, color): + """Return the "SGR" parameter for selecting color. + + See https://en.wikipedia.org/wiki/ANSI_escape_code#SGR for an + explanation. We use the basic codes for black/white/default for + maximum compatibility; they are the only colors used when the + mono palette is selected. + """ + if color == 'white': + return '7' + elif color == 'black': + return '0' + elif color == 'default': + return '9' + else: + # This is almost but not quite a ISO 8613-3 code -- that + # would use colons to separate the rgb values instead. But + # it's what xterm, and hence everything else, supports. + return '8;2;{};{};{}'.format(*self._urwid_name_to_rgb[color]) + + def _attrspec_to_escape(self, a): + return '\x1b[0;3{};4{}m'.format( + self._cc(a.foreground), + self._cc(a.background)) + + +_is_linux_tty = None + + +def is_linux_tty(): + global _is_linux_tty + if _is_linux_tty is None: + try: + r = fcntl.ioctl(sys.stdout.fileno(), KDGKBTYPE, ' ') + except IOError as e: + log.debug("KDGKBTYPE failed %r", e) + return False + log.debug("KDGKBTYPE returned %r, is_linux_tty %s", r, r == b'\x02') + _is_linux_tty = r == b'\x02' + return _is_linux_tty + + +def make_screen(ascii=False, inputf=None, outputf=None): + """ """ + if inputf is None: + inputf = sys.stdin + if outputf is None: + outputf = sys.stdout + if is_linux_tty(): + return LinuxScreen(COLORS, input=inputf, output=outputf) + elif ascii: + return SubiquityScreen(input=inputf, output=outputf) + else: + return TwentyFourBitScreen(COLORS, input=inputf, output=outputf)