[elbe-devel] [PATCH v2 01/13] Debianize - TUI

Bastian Germann bage at linutronix.de
Fri Aug 2 09:39:26 CEST 2019


> From: Olivier Dion <dion at linutronix.de>
> 
> The TUI uses the Urwid package, see <http://urwid.org/>.
> 
> The code is copy-paste from one of my previous collaborate project
> with a friend at <https://github.com/abelfodil/inf1900-grader> and is
> under GPL-3+.
> 
> I've also made some modifications to fit the debianizer needs.
> 
> * TUI
> 
>   The TUI is a singleton object that controls the terminal.  It
>   divides it in 3 sections.  The header, the body and the footer.  Is
>   also installs signal handlers to ensure proper restoration of the
>   terminal settings.
> 
> ** Header
> 
>    The header is a Text widget.  It's manipulated through the
>    TUI.print and TUI.clear method.
> 
> *** TUI.print
> 
>     Print a formatted text to the header.  The previous text is
>     overwritten.
> 
> *** TUI.clear
> 
>     Clear the header.
> 
> ** Footer
> 
>    The footer is a Text widget.  It's not intend to be manipulated.
>    It only shows useful keybinding to the user.
> 
> ** Body
> 
>    The body can be anything.  It's the main view of the application.
> 
> Signed-off-by: Olivier Dion <dion at linutronix.de>

Reviewed-by: Bastian Germann <bage at linutronix.de>

> ---
>  elbepack/debianize/base/tui.py | 180 +++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 180 insertions(+)
>  create mode 100644 elbepack/debianize/base/tui.py
> 
> diff --git a/elbepack/debianize/base/tui.py b/elbepack/debianize/base/tui.py
> new file mode 100644
> index 00000000..e0ca5e26
> --- /dev/null
> +++ b/elbepack/debianize/base/tui.py
> @@ -0,0 +1,180 @@
> +# ELBE - Debian Based Embedded Rootfilesystem Builder
> +# Copyright (c) 2019 Olivier Dion <dion at linutronix.de>
> +#
> +# SPDX-License-Identifier: GPL-3.0-or-later
> +
> +
> +import os
> +import signal
> +
> +from urwid import (
> +    ExitMainLoop,
> +    Frame,
> +    MainLoop,
> +    Text
> +)
> +
> +
> +# +=====================================================+
> +# |+-: root (box) -------------------------------------+|
> +# ||+-: header (flow) --------------------------------+||
> +# |||                                                 |||
> +# ||+-------------------------------------------------+||
> +# ||+-: body (box) -----------------------------------+||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# |||                                                 |||
> +# ||+-------------------------------------------------+||
> +# ||+-: helper (flow) --------------------------------+||
> +# |||                                                 | |
> +# ||+-------------------------------------------------+||
> +# |+---------------------------------------------------+|
> +# +=====================================================+
> +
> +
> +def generate_helper_text(hints):
> +    markup = []
> +    for key, text, text_palette in hints:
> +        markup.extend((("helper_key", key), " ", (text_palette, text), " "))
> +    return markup
> +
> +
> +class TUISignal(object):
> +    QUIT = "on_quit"
> +    CLICK = "on_click"
> +    FLUSH = "on_flush"
> +
> +class TUIException(Exception):
> +    pass
> +
> +
> +class TUI(object):
> +
> +    palette = [
> +        ("blue_head", "dark blue", ""),
> +        ("red_head", "dark red", ""),
> +        ("header", "bold, underline, default", ""),
> +        ("error", "bold, light red", ""),
> +        ("normal_box", "default", "default"),
> +        ("selected_box", "black", "light gray"),
> +        ("confirm_button", "default", "dark green"),
> +        ("abort_button", "light red", "dark red"),
> +        ("progress_low", "default", "yellow"),
> +        ("progress_hight", "default", "dark green"),
> +        ("helper_key", "bold", "default"),
> +        ("helper_text_brown", "black", "brown"),
> +        ("helper_text_red", "black", "dark red"),
> +        ("helper_text_green", "black", "dark green"),
> +        ("helper_text_light", "white", "dark blue"),
> +    ]
> +
> +    main_helper_text = generate_helper_text([
> +        ("C-f", "Forward", "helper_text_brown"),
> +        ("C-b", "Backward", "helper_text_brown"),
> +        ("C-p", "Previous", "helper_text_brown"),
> +        ("C-n", "Next", "helper_text_brown"),
> +        ("TAB", "Next", "helper_text_brown"),
> +        ("backtab", "Previous", "helper_text_brown"),
> +        ("C-\\", "Quit", "helper_text_red"),
> +    ])
> +
> +    keybind = {}
> +
> +    def __init__(self, body):
> +
> +        TUI.root = Frame(body.root,
> +                         Text(("header", ""), "center"),
> +                         Text(TUI.main_helper_text, "center"))
> +
> +        TUI.loop = MainLoop(TUI.root, TUI.palette,
> +                            unhandled_input=TUI.unhandled_input)
> +
> +        TUI.install_signals_handler()
> +
> +    def __call__(self):
> +        TUI.loop.run()
> +
> +    @classmethod
> +    def focus_header(cls):
> +        cls.root.focus_position = "header"
> +
> +    @classmethod
> +    def focus_body(cls):
> +        cls.root.focus_position = "body"
> +
> +    @classmethod
> +    def focus_footer(cls):
> +        cls.root.focus_position = "footer"
> +
> +    @classmethod
> +    def header(cls, flow_widget=None):
> +        if flow_widget is not None:
> +            if "flow" not in flow_widget.sizing():
> +                raise TUIException("Header must be of sizing flow")
> +            cls.root.contents["header"] = flow_widget
> +        return cls.root.contents["header"]
> +
> +    @classmethod
> +    def body(cls, box_widget=None):
> +        if box_widget is not None:
> +            if "box" not in box_widget.sizing():
> +                raise TUIException("Body must be of sizing box")
> +            cls.root.contents["body"] = (box_widget, TUI.root.options())
> +        return cls.root.contents["body"]
> +
> +    @classmethod
> +    def footer(cls, flow_widget=None):
> +        if flow_widget is not None:
> +            if "flow" not in flow_widget.sizing():
> +                raise TUIException("Header must be of sizing flow")
> +            cls.root.contents["footer"] = flow_widget
> +        return cls.root.contents["footer"]
> +
> +    @classmethod
> +    def unhandled_input(cls, key):
> +        if key in cls.keybind:
> +            cls.keybind[key]()
> +            return None
> +
> +    @classmethod
> +    def bind_global(cls, key, callback):
> +        cls.keybind[key] = callback
> +
> +    @classmethod
> +    def printf(cls, fmt, *args):
> +        cls.header()[0].set_text(("header", fmt.format(*args)))
> +
> +    @classmethod
> +    def clear(cls):
> +        cls.printf("")
> +
> +    @staticmethod
> +    def quit(*args):
> +        raise ExitMainLoop()
> +
> +    @staticmethod
> +    def pause(*args):
> +        TUI.loop.stop()
> +        os.kill(os.getpid(), signal.SIGSTOP)
> +        TUI.loop.start()
> +        TUI.loop.draw_screen()
> +
> +    @staticmethod
> +    def interrupt(*kargs):
> +        pass
> +
> +    @staticmethod
> +    def install_signals_handler():
> +
> +        if os.sys.platform != "win32":
> +            signal.signal(signal.SIGQUIT, TUI.quit)
> +            signal.signal(signal.SIGTSTP, TUI.pause)
> +
> +        signal.signal(signal.SIGINT, TUI.interrupt)
> 



More information about the elbe-devel mailing list