Merge pull request #804 from mwhudson/screen-refactor

Move the code for setting up the screen to its own file
This commit is contained in:
Michael Hudson-Doyle 2020-08-23 22:43:42 +12:00 committed by GitHub
commit 63f5f57f30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 208 additions and 172 deletions

View File

@ -25,8 +25,6 @@ log = logging.getLogger("console_conf.core")
class ConsoleConf(Application): class ConsoleConf(Application):
from subiquitycore.palette import COLORS, STYLES, STYLES_MONO
project = "console_conf" project = "console_conf"
make_model = ConsoleConfModel make_model = ConsoleConfModel
@ -40,8 +38,6 @@ class ConsoleConf(Application):
class RecoveryChooser(Application): class RecoveryChooser(Application):
from subiquitycore.palette import COLORS, STYLES, STYLES_MONO
project = "console_conf" project = "console_conf"
controllers = [ controllers = [

View File

@ -16,6 +16,8 @@
import logging import logging
import os import os
from subiquitycore.screen import is_linux_tty
from subiquity.controller import SubiquityController from subiquity.controller import SubiquityController
from subiquity.ui.views import WelcomeView from subiquity.ui.views import WelcomeView
@ -39,7 +41,7 @@ class WelcomeController(SubiquityController):
lang = os.environ.get("LANG") lang = os.environ.get("LANG")
if lang is not None and lang.endswith(".UTF-8"): if lang is not None and lang.endswith(".UTF-8"):
lang = lang.rsplit('.', 1)[0] 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: if code == lang:
self.model.switch_language(code) self.model.switch_language(code)
break break

View File

@ -85,8 +85,6 @@ class Subiquity(Application):
'additionalProperties': True, 'additionalProperties': True,
} }
from subiquitycore.palette import COLORS, STYLES, STYLES_MONO
project = "subiquity" project = "subiquity"
def make_model(self): def make_model(self):

View File

@ -25,6 +25,7 @@ from urwid import Text
from subiquitycore.ui.buttons import forward_btn, other_btn from subiquitycore.ui.buttons import forward_btn, other_btn
from subiquitycore.ui.container import ListBox from subiquitycore.ui.container import ListBox
from subiquitycore.ui.utils import button_pile, rewrap, screen from subiquitycore.ui.utils import button_pile, rewrap, screen
from subiquitycore.screen import is_linux_tty
from subiquitycore.view import BaseView from subiquitycore.view import BaseView
from subiquity.ui.views.help import ( from subiquity.ui.views.help import (
@ -64,7 +65,6 @@ class WelcomeView(BaseView):
def __init__(self, model, controller): def __init__(self, model, controller):
self.model = model self.model = model
self.controller = controller 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: if controller.app.opts.run_on_serial and not controller.app.rich_mode:
s = self.make_serial_choices() s = self.make_serial_choices()
self.title = "Welcome!" self.title = "Welcome!"
@ -75,7 +75,7 @@ class WelcomeView(BaseView):
def make_language_choices(self): def make_language_choices(self):
btns = [] btns = []
current_index = None 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 cur = self.model.selected_language
log.debug("_build_model_inputs selected_language=%s", cur) log.debug("_build_model_inputs selected_language=%s", cur)
if cur in ["C", None]: if cur in ["C", None]:

View File

@ -20,7 +20,6 @@ import logging
import os import os
import struct import struct
import sys import sys
import tty
import urwid import urwid
import yaml import yaml
@ -32,21 +31,16 @@ from subiquitycore.context import (
from subiquitycore.controller import ( from subiquitycore.controller import (
Skip, Skip,
) )
from subiquitycore.signals import Signal from subiquitycore.palette import PALETTE_COLOR, PALETTE_MONO
from subiquitycore.prober import Prober 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.ui.frame import SubiquityCoreUI
from subiquitycore.utils import arun_command from subiquitycore.utils import arun_command
log = logging.getLogger('subiquitycore.core') 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 # /usr/include/linux/kd.h
K_RAW = 0x00 K_RAW = 0x00
K_XLATE = 0x01 K_XLATE = 0x01
@ -58,86 +52,6 @@ KDGKBMODE = 0x4B44 # gets current keyboard mode
KDSKBMODE = 0x4B45 # sets 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(): def extend_dec_special_charmap():
urwid.escape.DEC_SPECIAL_CHARMAP.update({ urwid.escape.DEC_SPECIAL_CHARMAP.update({
ord('\N{BLACK RIGHT-POINTING SMALL TRIANGLE}'): '>', 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 # Set rich_mode to the opposite of what we want, so we can
# call toggle_rich to get the right things set up. # call toggle_rich to get the right things set up.
self.rich_mode = opts.run_on_serial self.rich_mode = opts.run_on_serial
self.color_palette = make_palette(self.COLORS, self.STYLES)
self.is_linux_tty = is_linux_tty() if is_linux_tty():
if self.is_linux_tty:
self.input_filter = KeyCodesFilter() self.input_filter = KeyCodesFilter()
else: else:
self.input_filter = DummyKeycodesFilter() self.input_filter = DummyKeycodesFilter()
@ -369,29 +280,10 @@ class Application:
**kw): **kw):
screen = self.urwid_loop.screen 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(): async def _run():
await arun_command( await arun_command(
cmd, stdin=None, stdout=None, stderr=None, **kw) cmd, stdin=None, stdout=None, stderr=None, **kw)
screen.start() screen.start()
urwid.emit_signal(
screen, urwid.display_common.INPUT_DESCRIPTORS_CHANGED)
self.setraw()
if after_hook is not None: if after_hook is not None:
after_hook() after_hook()
@ -552,11 +444,11 @@ class Application:
def toggle_rich(self): def toggle_rich(self):
if self.rich_mode: if self.rich_mode:
urwid.util.set_encoding('ascii') urwid.util.set_encoding('ascii')
new_palette = self.STYLES_MONO new_palette = PALETTE_MONO
self.rich_mode = False self.rich_mode = False
else: else:
urwid.util.set_encoding('utf-8') urwid.util.set_encoding('utf-8')
new_palette = self.color_palette new_palette = PALETTE_COLOR
self.rich_mode = True self.rich_mode = True
urwid.CanvasCache.clear() urwid.CanvasCache.clear()
self.urwid_loop.screen.register_palette(new_palette) self.urwid_loop.screen.register_palette(new_palette)
@ -598,53 +490,14 @@ class Application:
controller.configured() controller.configured()
return controller_index 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): def make_screen(self, inputf=None, outputf=None):
"""Return a screen to be passed to MainLoop. return make_screen(self.opts.ascii, inputf, outputf)
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)
def run(self, input=None, output=None): def run(self, input=None, output=None):
log.debug("Application.run") log.debug("Application.run")
screen = self.make_screen(input, output)
self.urwid_loop = urwid.MainLoop( 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, handle_mouse=False, pop_ups=True,
input_filter=self.input_filter.filter, input_filter=self.input_filter.filter,
unhandled_input=self.unhandled_input, unhandled_input=self.unhandled_input,
@ -666,7 +519,6 @@ class Application:
if self.updated: if self.updated:
initial_controller_index = self.load_serialized_state() initial_controller_index = self.load_serialized_state()
self.aio_loop.call_soon(self.setraw)
self.aio_loop.call_soon( self.aio_loop.call_soon(
self.select_initial_screen, initial_controller_index) self.select_initial_screen, initial_controller_index)
self._connect_base_signals() self._connect_base_signals()

View File

@ -1,4 +1,4 @@
# Copyright 2015 Canonical, Ltd. # Copyright 2020 Canonical, Ltd.
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Palette definitions """
COLORS = [ COLORS = [
# black # black
("bg", (0x11, 0x11, 0x11)), ("bg", (0x11, 0x11, 0x11)),
# dark read # dark red
("danger", (0xff, 0x00, 0x00)), ("danger", (0xff, 0x00, 0x00)),
# dark green # dark green
("good", (0x0e, 0x84, 0x20)), ("good", (0x0e, 0x84, 0x20)),
@ -35,7 +33,7 @@ COLORS = [
("fg", (0xff, 0xff, 0xff)), ("fg", (0xff, 0xff, 0xff)),
] ]
STYLES = [ PALETTE_COLOR = [
('frame_header_fringe', 'orange', 'bg'), ('frame_header_fringe', 'orange', 'bg'),
('frame_header', 'fg', 'orange'), ('frame_header', 'fg', 'orange'),
('body', 'fg', 'bg'), ('body', 'fg', 'bg'),
@ -71,8 +69,7 @@ STYLES = [
('verified focus', 'good', 'gray'), ('verified focus', 'good', 'gray'),
] ]
PALETTE_MONO = [
STYLES_MONO = [
('frame_header_fringe', 'white', 'black'), ('frame_header_fringe', 'white', 'black'),
('frame_header', 'black', 'white'), ('frame_header', 'black', 'white'),
('body', 'white', 'black'), ('body', 'white', 'black'),
@ -102,3 +99,43 @@ STYLES_MONO = [
('scrollbar_fg', 'white', 'black'), ('scrollbar_fg', 'white', 'black'),
('scrollbar_bg', '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)

151
subiquitycore/screen.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)