feat(Anaconda): Local Repo

This commit is contained in:
2026-06-13 02:06:34 +02:00
parent 05ae9c36c3
commit c412cd5d33
203 changed files with 23997 additions and 7 deletions
@@ -0,0 +1,31 @@
# Module with classes to handle rendering and input.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline.errors import SimplelineError
class RenderError(SimplelineError):
"""Exception raised when error in rendering happens."""
class RenderUnexpectedError(RenderError):
"""Exception raised when something goes really wrong."""
@@ -0,0 +1,253 @@
# Advanced widgets
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
import sys
from simpleline.render import widgets
from simpleline.render.containers import WindowContainer
from simpleline.render.prompt import Prompt
from simpleline.render.screen import UIScreen, InputState
from simpleline.input.input_handler import PasswordInputHandler
from simpleline.utils.i18n import _, N_, C_
__all__ = ["ErrorDialog", "GetInputScreen", "GetPasswordInputScreen", "HelpScreen",
"PasswordDialog", "YesNoDialog"]
class ErrorDialog(UIScreen):
"""Dialog screen for reporting errors to user."""
def __init__(self, message):
"""
:param message: the message to show to the user
:type message: str
"""
super().__init__()
self.title = N_("Error")
self._message = message
def refresh(self, args=None):
super().refresh(args)
text = widgets.TextWidget(self._message)
self.window.add_with_separator(widgets.CenterWidget(text))
def prompt(self, args=None):
return Prompt(_("Press %s to exit") % Prompt.ENTER)
def input(self, args, key):
"""This dialog is closed by any input.
And causes the program to quit.
"""
sys.exit(1)
class PasswordDialog(UIScreen):
"""Dialog screen for password input."""
def __init__(self, message=None):
"""
:param message: password prompt question
:type message: string
"""
super().__init__()
self.title = N_("Password")
self._message = message or _("Enter your passphrase")
self._password = None
def refresh(self, args=None):
super().refresh(args)
text = widgets.TextWidget(self._message)
self.window.add_with_separator(widgets.CenterWidget(text))
def prompt(self, args=None):
handler = PasswordInputHandler(source=self)
if self.password_func:
handler.set_pass_func(self.password_func)
handler.get_input(_("Passphrase: "))
handler.wait_on_input()
if not handler.input_successful():
return None
self._password = handler.value
# this may seem innocuous, but it's really a giant hack; we should
# not be calling close() from prompt(), but the input handling code
# in the TUI is such that without this very simple workaround, we
# would be forever pelting users with a prompt to enter their pw
self.close()
return None
@property
def answer(self):
"""The response can be None (no response) or the password entered."""
return self._password
def input(self, args, key):
if key:
self._password = key
return InputState.PROCESSED_AND_CLOSE
return InputState.DISCARDED
class YesNoDialog(UIScreen):
"""Dialog screen for Yes - No questions."""
def __init__(self, message):
"""
:param message: the message to show to the user
:type message: unicode
"""
super().__init__()
self.title = N_("Question")
self._message = message
self._response = None
def refresh(self, args=None):
super().refresh(args)
text = widgets.TextWidget(self._message)
self.window.add_with_separator(widgets.CenterWidget(text))
def prompt(self, args=None):
return Prompt(_("Please respond '%(yes)s' or '%(no)s'") % {
# TRANSLATORS: 'yes' as positive reply
"yes": C_('TUI|Spoke Navigation', 'yes'),
# TRANSLATORS: 'no' as negative reply
"no": C_('TUI|Spoke Navigation', 'no')
})
def input(self, args, key):
# TRANSLATORS: 'yes' as positive reply
if key == C_('TUI|Spoke Navigation', 'yes'):
self._response = True
return InputState.PROCESSED_AND_CLOSE
# TRANSLATORS: 'no' as negative reply
if key == C_('TUI|Spoke Navigation', 'no'):
self._response = False
return InputState.PROCESSED_AND_CLOSE
return InputState.DISCARDED
@property
def answer(self):
"""The response can be True (yes), False (no) or None (no response)."""
return self._response
class HelpScreen(UIScreen):
"""Screen to display a help message."""
def __init__(self, help_path):
"""
:param help_path: help file name
:type help_path: str
"""
super().__init__()
self.title = N_("Help")
self.help_path = help_path
def refresh(self, args=None):
""" Show the help. """
super().refresh(args)
help_message = _("The help is not available.")
if self.help_path:
with open(self.help_path, 'r') as f:
help_message = f.read()
self.window.add_with_separator(widgets.TextWidget(help_message))
def input(self, args, key):
""" Handle user input. """
return InputState.PROCESSED_AND_CLOSE
def prompt(self, args=None):
return Prompt(_("Press %s to return") % Prompt.ENTER)
class GetInputScreen(UIScreen):
"""Screen for getting user input."""
def __init__(self, message):
"""
:param message: Prompt printed before user input.
:type message: str
"""
super().__init__()
self._message = message
self._value = None
self._conditions = []
@property
def value(self):
"""User input."""
return self._value
def add_acceptance_condition(self, acceptance_function, args=None):
"""Add acceptance condition to the conditions list.
:param acceptance_function: Functions that accepts or rejects a user input.
:type acceptance_function: `function(input, args) -> bool` - function which takes
user input (string) and arguments (`args`) and return True when
input is accepted or False if rejected so we will ask for
a new input.
:param args: Second argument for `acceptance_function` the first one will be user input.
:type args: Anything.
"""
self._conditions.append((acceptance_function, args))
def clear_acceptance_conditions(self):
"""Clear list of the acceptance conditions."""
self._conditions.clear()
def refresh(self, args=None):
super().refresh(args)
self._window = WindowContainer()
def prompt(self, args=None):
return Prompt(message=self._message)
def input(self, args, key):
if not self._test_input(key):
return InputState.DISCARDED
self._value = key
return InputState.PROCESSED_AND_CLOSE
def _test_input(self, key):
for f, args in self._conditions:
if not f(key, args):
return False
return True
class GetPasswordInputScreen(GetInputScreen):
"""Screen for getting user password input."""
def __init__(self, message):
super().__init__(message)
self.hide_user_input = True
@@ -0,0 +1,475 @@
# Widgets for holding other widgets.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
from math import ceil
from simpleline.render.widgets import Widget, TextWidget, SeparatorWidget
from simpleline.logging import get_simpleline_logger
__all__ = ["ListRowContainer", "ListColumnContainer", "WindowContainer"]
log = get_simpleline_logger()
class Container(Widget):
"""Base class for containers which will do positioning of the widgets."""
def __init__(self, items=None, numbering=True):
"""Construct Container.
:param items: List of items for positioning in this Container. Callback
can't be specified this way.
:type items: List of items for rendering.
:param numbering: Enable/disable automatic numbering (labels) for items.
Enabled by default (True).
:type numbering: bool
"""
super().__init__()
self._key_pattern = None
self._items = []
if items:
for i in items:
self._items.append(ContainerItem(i))
if numbering:
self._key_pattern = KeyPattern()
else:
self._key_pattern = None
@property
def size(self):
"""Return items count."""
return len(self._items)
@property
def key_pattern(self):
"""Return key pattern which will be used for items numbering.
Will return `None` if not set.
"""
return self._key_pattern
@key_pattern.setter
def key_pattern(self, key_pattern):
"""Set the key pattern object which will be used for items numbering.
Setting `None` will stop doing numbering.
"""
self._key_pattern = key_pattern
def add(self, item, callback=None, data=None):
"""Add item to the Container.
:param item: Add item to this container.
:type item: Could be item (based on `simpleline.render.widgets.Widget`)
or other container (based on `simpleline.render.containers.Container`).
:param callback: Add callback for this item. This callback will be called when user
activate this `item`.
:type callback: function ``func(data)``.
:param data: Data which will be passed to the callback.
:param data: Anything.
:returns: ID of the item in this Container.
:rtype: int
"""
self._items.append(ContainerItem(item, callback, data))
return len(self._items) - 1
def process_user_input(self, key):
"""Process input from the user if any of the items in the list was called.
This method must be called in `UIScreen.input()` method if list widget should call
the callbacks.
:param key: Key pressed from user.
:type key: str
:returns: True if key was processed. False otherwise.
"""
if not self._key_pattern:
return False
if not isinstance(key, str):
return False
res = self._key_pattern.translate_input_to_widget_id(key)
if res is not None and res >= 0:
try:
item = self._items[res]
if item.callback is not None:
item.callback(item.data)
return True
except IndexError: # container widget with this id doesn't exists
return False
return False
def create_number_label(self, item_id):
"""Create TextWidget from KeyPattern.
:param item_id: Create label for item with this id.
:type item_id: int
:returns: Widget with label for the item with item_id.
:rtype: `simpleline.render.widgets.TextWidget` instance.
"""
number_widget = TextWidget(self._key_pattern.get_widget_label(item_id))
return number_widget
class WindowContainer(Container):
"""Base container for screens.
This can hold other containers or Widgets for rendering.
"""
def __init__(self, title=None):
"""Construct base container for screens.
This container doesn't have numbering support. Input other containers in it to
allow numbering and input processing.
:param title: Title line with separator after this title.
:type title: str
"""
super().__init__(numbering=False)
self._title = title
def add_with_separator(self, item, callback=None, data=None, blank_lines=1):
"""Add widget and after widget add blank line.
This method will call
`self.add(item, callback, data)`
`self.add_separator(lines)`
:param item: Add item to this container.
:type item: Could be item (based on `simpleline.render.widgets.Widget`)
or other container (based on `simpleline.render.containers.Container`).
:param callback: Add callback for this item. This callback will be called when user
activate this `item`.
:type callback: function ``func(data)``.
:param data: Data which will be passed to the callback.
:param data: Anything.
:param blank_lines: How many blank lines should be printed.
:type blank_lines: int greater than 0.
:returns: ID of the item in this Container.
:rtype: int
"""
item_id = self.add(item, callback, data)
self.add_separator(blank_lines)
return item_id
def add_separator(self, lines=1):
"""Add blank lines between widgets.
:param lines: How many blank lines should be printed.
:type lines: int greater than 0.
"""
self.add(SeparatorWidget(lines))
@property
def title(self):
"""Title of WindowContainer."""
return self._title
def render(self, width):
"""Render widgets to it's internal buffer.
:param width: the maximum width the item can use
:type width: int
:return: nothing
"""
super().render(width)
# set cursor position to top-left corner
self.set_cursor_position(0, 0)
if self._title:
self._draw_title_and_separator(width)
for item in self._items:
widget = item.widget
widget.render(width)
self.draw(widget)
def _draw_title_and_separator(self, width):
title_widget = TextWidget(self._title)
sep = SeparatorWidget()
title_widget.render(width)
sep.render(width)
self.draw(title_widget)
self.draw(sep)
class ListRowContainer(Container):
"""Place widgets in rows automatically.
Compared to the ColumnWidget this is able to handle word wrapping correctly.
There is numbering N) automatically for all items. To disable this feature call
`self.key_pattern = None`. If you want other numbering then look on `KeyPattern` class.
Widgets will be placed based on the number of columns in the following way:
1) w1 2) w2 3) w3
4) w4 5) w5 6) w6
....
"""
def __init__(self, columns, items=None, columns_width=None, spacing=3, numbering=True):
"""Create ListWidget with specific number of columns.
:param columns: How many columns we want.
:type columns: int, bigger than 0
:param items: List of items for positioning in this Container. Callback can't be
specified this way.
:type items: List of items for rendering.
:param columns_width: Width of every column. If nothing specified the maximum width
will be distributed to columns.
:type columns_width: int or None
:param spacing: Set the spacing between columns.
:type spacing: int
:param numbering: Enable/disable automatic numbering (labels) for items.
Enabled by default (True).
:type numbering: bool
"""
super().__init__(items, numbering)
self._columns = columns
self._columns_width = columns_width
self._spacing = spacing
self._numbering_widgets = []
def render(self, width):
"""Render widgets to it's internal buffer.
:param width: the maximum width the item can use
:type width: int
:return: nothing
"""
super().render(width)
if self._columns_width is None:
spaces_between_columns = self._columns - 1
sum_spacing = spaces_between_columns * self._spacing
self._columns_width = int((width - sum_spacing) / self._columns)
ordered_map = self._get_ordered_map()
lines_per_rows = self._lines_per_every_row(ordered_map)
# the leftmost empty column
col_pos = 0
for col in ordered_map:
row_pos = 0
# render and draw contents of column
for row_id, item_id in enumerate(col):
container = self._items[item_id]
widget = container.widget
# set cursor to first line and leftmost empty column
self.set_cursor_position(row_pos, col_pos)
if self._key_pattern is not None:
number_widget = self._numbering_widgets[item_id]
widget_width = len(number_widget.text)
self.draw(number_widget)
self.set_cursor_position(row_pos, col_pos + widget_width)
self.draw(widget, block=True)
row_pos = row_pos + lines_per_rows[row_id]
# recompute the leftmost empty column
col_pos = max((col_pos + self._columns_width), self.width) + self._spacing
def _lines_per_every_row(self, items):
self._render_all_items()
# call `self._render_and_calculate_lines_per_rows()` method instead
lines_per_row = []
# go through all items and find how many lines we need for each row
# printed (because of wrapping)
for column_items in items:
for row_id, item_id in enumerate(column_items):
item = self._items[item_id]
if len(lines_per_row) <= row_id:
lines_per_row.append(0)
lines_per_row[row_id] = max(lines_per_row[row_id], len(item.widget.get_lines()))
return lines_per_row
def _render_all_items(self):
for item_id, item in enumerate(self._items):
item_width = self._columns_width
if item_width <= 0:
raise ValueError("Widget can't be rendered! Columns width is too small.")
if self._key_pattern:
number_widget = self.create_number_label(item_id)
# render numbers before widgets
number_width = len(number_widget.text)
number_widget.render(number_width)
self._numbering_widgets.append(number_widget)
# reduce the size of widget because of the number
item_width -= number_width
if item_width <= 0:
raise ValueError("Widget can't be rendered with numbering on! "
"Increase column width or disable numbering.")
item.widget.render(item_width)
def _get_ordered_map(self):
"""Return list of identifiers (index) to the original item list.
.. NOTE: Use of ``self._prepare_list()` is encouraged to create output list and
just fill up this list.
"""
# create list of columns (lists)
ordering_map = self._prepare_list()
for item_id in range(self.size):
ordering_map[item_id % self._columns].append(item_id)
return ordering_map
def _prepare_list(self):
"""Prepare list for items ordering to rows and columns.
List will be prepared as ([column 1], [column 2], ...)
"""
return list(map(lambda x: [], range(0, self._columns)))
class ListColumnContainer(ListRowContainer):
"""Place widgets in columns automatically.
Compared to the ColumnWidget this is able to handle word wrapping correctly.
There is numbering N) automatically for all items. To disable this feature call
`self.key_pattern = None`. If you want other numbering then look on `KeyPattern` class.
Widgets will be placed based on the number of columns in the following way:
1) w1 4) w4 7) w7
2) w2 5) w5 8) w8
3) w3 6) w6 9) w9
"""
def _get_ordered_map(self):
ordering_map = self._prepare_list()
items_in_column = ceil(len(self._items) / self._columns)
for item_id in range(self.size):
col_position = int(item_id // items_in_column)
ordering_map[col_position].append(item_id)
return ordering_map
class KeyPattern():
"""Pattern for automatic key printing before items."""
def __init__(self, pattern="{:d}) ", offset=1):
"""Create the pattern class.
For enabling greater functionality than python 3 format is able to do, feel free to
override this class and use your subclass instead.
:param pattern: Set pattern which will be called for every item.
:type pattern: Strings format method.
See https://docs.python.org/3.3/library/string.html#format-string-syntax.
:param offset: Set the offset for numbering items. Default is 1 to start indexing
naturally for user.
:type offset: int
"""
self._pattern = pattern
self._offset = offset
def get_widget_label(self, item_id):
"""Get widget identifier for user input description.
It should be something similar to the pattern.
:param item_id: Position of the widget in the list.
:type item_id: int starts from 0.
"""
return self._pattern.format(item_id + self._offset)
def translate_input_to_widget_id(self, user_input):
"""Get id of the widget from the user input.
This is reverse translation to `self.get_widget_identifier()`.
:param user_input: Input from user:
:type user_input: str
:return: ID of the widget in the list or None if the input can't be translated.
:rtype: int or None
"""
try:
return int(user_input) - 1
except ValueError:
log.debug("No callback registered for user input %s", user_input)
return None
class ContainerItem():
"""Item used inside of containers to store widgets callbacks and data.
Internal representation for Containers. Do not use this class directly.
"""
def __init__(self, widget, callback=None, data=None):
"""Construct WidgetContainer.
:param widget: Any item from `simpleline.render.widgets` or `Container`.
:type widget: Class subclassing the `simpleline.render.widgets.Widget` class
or `simpleline.render.containers.Container`.
:param callback: This callback will be called as reaction on user input.
:type callback: Function with one data parameter: `def func(data):`.
:param data: Params which will be passed to callback.
:type data: Anything.
"""
self.widget = widget
self.callback = callback
self.data = data
@@ -0,0 +1,154 @@
# Class for the Anaconda TUI prompt.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Vendula Poncova <vponcova@redhat.com>
#
import logging
from simpleline.utils.i18n import N_, _
log = logging.getLogger("simpleline")
class Prompt():
"""Class to create a prompt message with options."""
# Default message of the prompt
DEFAULT_MESSAGE = N_("Please make a selection from the above")
# String to use in a prompt when we want users to press the key ENTER.
ENTER = N_("ENTER")
# TRANSLATORS: 'q' to quit
QUIT_DESCRIPTION = N_("to quit")
QUIT = 'q'
# TRANSLATORS:'c' to continue
CONTINUE_DESCRIPTION = N_("to continue")
CONTINUE = 'c'
# TRANSLATORS:'r' to refresh
REFRESH_DESCRIPTION = N_("to refresh")
REFRESH = 'r'
# TRANSLATORS:'h' to help
HELP_DESCRIPTION = N_("to help")
HELP = 'h'
def __init__(self, message=DEFAULT_MESSAGE):
"""
:param message: the message of the prompt
:type message: str|None
"""
self.message = message
self.options = dict()
def set_message(self, message):
"""Set the prompt message.
:param message: the message of the prompt
:type message: str|None
"""
self.message = message
def add_option(self, key, description):
"""Add an option to the prompt.
Causes a warning if the option already exists.
:param key: the key for choosing the option
:type key: str
:param description: the description of the option
:type description: str
"""
if key in self.options:
log.warning("The option '%s' does already exist in '%s'.", key, self)
self.options[key] = description
def update_option(self, key, description):
"""Update an option in the prompt.
Causes a warning if the option does not exist.
:param key: the key for choosing the option
:type key: str
:param description: the description of the option
:type description: str
"""
if key not in self.options:
log.warning("The option '%s' does not exist in '%s'.", key, self)
self.options[key] = description
def add_refresh_option(self, description=REFRESH_DESCRIPTION):
"""Add the option to refresh."""
if Prompt.REFRESH in self.options:
self.update_option(Prompt.REFRESH, description)
else:
self.add_option(Prompt.REFRESH, description)
def add_continue_option(self, description=CONTINUE_DESCRIPTION):
"""Add the option to continue."""
if Prompt.CONTINUE in self.options:
self.update_option(Prompt.CONTINUE, description)
else:
self.add_option(Prompt.CONTINUE, description)
def add_quit_option(self, description=QUIT_DESCRIPTION):
"""Add the option to quit."""
if Prompt.QUIT in self.options:
self.update_option(Prompt.QUIT, description)
else:
self.add_option(Prompt.QUIT, description)
def add_help_option(self, description=HELP_DESCRIPTION):
"""Add the option to help."""
if Prompt.HELP in self.options:
self.update_option(Prompt.HELP, description)
else:
self.add_option(Prompt.HELP, description)
def remove_option(self, key):
"""Remove an option with the given key.
:param key: the key of the option
:type key: str
:return: the removed option
:rtype: str|None
"""
return self.options.pop(key, None)
def __str__(self):
"""Return the string representation of the prompt."""
if not self.message and not self.options:
return ""
parts = []
if self.message:
parts.append(_(self.message))
if self.options:
opt_list = ["'%s' %s" % (key, _(self.options[key]))
for key in sorted(self.options.keys())]
opt_str = "[%s]" % ", ".join(opt_list)
parts.append(opt_str)
return " ".join(parts) + ": "
@@ -0,0 +1,313 @@
# Base class for text window screens.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from enum import Enum
from simpleline import App
from simpleline.render.containers import WindowContainer
from simpleline.render.prompt import Prompt
from simpleline.render.screen.signal_handler import SignalHandler
from simpleline.render.screen.input_manager import InputManager
from simpleline.utils.i18n import _
__all__ = ["UIScreen", "InputState"]
class UIScreen(SignalHandler):
"""Base class representing one TUI Screen.
Shares some API with anaconda's GUI to make it easy for devs to create similar UI
with the familiar API.
"""
def __init__(self, title=None, screen_height=30):
""" Constructor of the TUI screen.
:param title: Title line of the screen.
:type title: str
:param screen_height: height of the screen (useful for printing long widgets)
:type screen_height: int (the value must be bigger than 4)
"""
self._title = title
self._screen_height = screen_height
self._screen_ready = False
# ask for password
self._hide_user_input = False
self._password_func = None
# do not print separator for this screen
self._no_separator = False
# list that holds the content to be printed out
self._window = WindowContainer(self.title)
# should the input be required after draw
self._input_required = True
# index of the page (subset of screen) shown during show_all
# indexing starts with 0
self._page = 0
self._input_manager = InputManager(ui_screen=self)
def __str__(self):
"""For easier logging."""
return self.__class__.__name__
@property
def title(self):
"""Screen title."""
return self._title
@title.setter
def title(self, title):
"""Set screen title.
Set `None` to remove title.
"""
self._title = title
@property
def password_func(self):
"""Get password function.
This is function with one argument to get password from command line.
"""
return self._password_func
@password_func.setter
def password_func(self, value):
"""Set password function.
:param value: Function to get password from a command line.
:type value: Function with one argument which is text representation of prompt.
"""
self._password_func = value
@property
def screen_ready(self):
"""This screen is ready for use."""
return self._screen_ready
@screen_ready.setter
def screen_ready(self, screen_ready):
"""Set ready status for this screen."""
self._screen_ready = screen_ready
@property
def input_required(self):
"""Return if the screen requires input."""
return self._input_required
@input_required.setter
def input_required(self, input_required):
"""Set if the screen should require input."""
self._input_required = input_required
@property
def no_separator(self):
"""Should we print separator for this screen?
:returns: True to print separator before this screen (default).
False do not print separator.
"""
return self._no_separator
@no_separator.setter
def no_separator(self, no_separator):
"""Print or do not print separator.
:param no_separator: Specify if the separator should be printed.
:type no_separator: bool (default: False).
"""
self._no_separator = no_separator
@property
def hide_user_input(self):
"""Hide typed user input.
This is main solution how to ask for password.
:returns: True if user input should be hidden.
False otherwise (default).
"""
return self._hide_user_input
@hide_user_input.setter
def hide_user_input(self, hide_input):
"""Should be the user input hidden.
:param hide_input: True if user input should be hidden.
False if not (default).
:type hide_input: bool (default: False).
"""
self._hide_user_input = hide_input
@property
def window(self):
"""Return WindowContainer instance."""
return self._window
@window.setter
def window(self, window):
"""Set base WindowContainer instance.
:param window: Base window container containing other widgets and containers.
:type window: Instance of `simpleline.render.containers.WindowContainer` class.
"""
self._window = window
def get_user_input(self, message, hidden=False):
"""Get immediately input from the user.
Use this with cautious. Never call this in middle of rendering or when other
input is already waiting. It is recommended to use `self.input_required` instead.
:param message: Message prompt for the user.
:type message: str
:param hidden: Do not echo user input (password typing).
:type hidden: bool
"""
return self._input_manager.get_input_blocking(message, hidden)
def setup(self, args):
"""Do additional setup right before this screen is used.
It is mandatory to call this ancestor method in the child class to set ready status.
:param args: arguments for the setup
:type args: array of values
:return: whether this screen should be scheduled or not
:rtype: bool
"""
self._screen_ready = True
App.get_event_loop().register_signal_source(self)
return True
def refresh(self, args=None):
"""Method which prepares the content desired on the screen to `self.window`.
:param args: optional argument passed from switch_screen calls
:type args: anything
"""
self.window = WindowContainer(self._title)
def _print_widget(self, widget):
"""Prints a widget with user interaction (when needed).
Could be longer than the screen height.
:param widget: widget to print
:type widget: Widget instance
"""
# TODO: Work even for lower screen_height than 4
pos = 0
lines = widget.get_lines()
num_lines = len(lines)
if num_lines == 0:
return
prompt_height = 2
real_screen_height = self._screen_height - prompt_height
if num_lines < real_screen_height:
# widget plus prompt are shorter than screen height, just print the widget
print(u"\n".join(lines))
return
# long widget, print it in steps and prompt user to continue
last_line = num_lines - 1
while pos <= last_line:
if pos + real_screen_height > last_line:
# enough space to print the rest of the widget plus regular
# prompt (2 lines)
for line in lines[pos:]:
print(line)
pos += self._screen_height - 1
else:
# print part with a prompt to continue
for line in lines[pos:(pos + real_screen_height)]:
print(line)
custom_prompt = Prompt(_("\nPress %s to continue") % Prompt.ENTER)
self._ask_user_input_blocking(custom_prompt)
pos += real_screen_height
def _ask_user_input_blocking(self, prompt):
return self._input_manager.get_input_blocking(prompt, False)
def show_all(self):
"""Print WindowContainer in `self.window` with all its content."""
self.window.render(App.get_configuration().width)
self._print_widget(self.window)
def input(self, args, key):
"""Method called to process input. If the input is not handled here, return it.
:param key: input string to process
:type key: str
:param args: optional argument passed from switch_screen calls
:type args: anything
:return: return `simpleline.render.InputState.PROCESSED` if key was handled,
`simpleline.render.InputState.DISCARDED` if the screen should not process input
on the scheduler and key if you want it to.
:rtype: `simpleline.render.InputState` enum | str
"""
return key
def get_input_with_error_check(self, args):
"""Get user input and redraw if user add too many invalid inputs.
This method should be used only by ScreenScheduler.
:param args: Arguments passed in when scheduling this screen.
:type args: Anything.
"""
self._input_manager.get_input(args=args)
def prompt(self, args=None):
"""Return the text to be shown as prompt or handle the prompt and return None.
:param args: optional argument passed from switch_screen calls
:type args: anything
:return: returns an instance of Prompt with text to be shown next to the prompt
for input or None to skip further input processing
:rtype: Prompt instance|None
"""
prompt = Prompt()
prompt.add_refresh_option()
prompt.add_continue_option()
prompt.add_quit_option()
return prompt
def closed(self):
"""Callback when this screen is closed."""
class InputState(Enum):
PROCESSED = 1
PROCESSED_AND_REDRAW = 2
PROCESSED_AND_CLOSE = 3
DISCARDED = 0
@@ -0,0 +1,203 @@
# Class for managing input and output for application.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from enum import Enum
from simpleline import App
from simpleline.event_loop import ExitMainLoop
from simpleline.event_loop.signals import ExceptionSignal
from simpleline.render.prompt import Prompt
from simpleline.input import InputHandler, PasswordInputHandler
from simpleline.logging import get_simpleline_logger
log = get_simpleline_logger()
class InputManager():
def __init__(self, ui_screen):
"""Processor for user input.
This class is mainly helper class for ScreenScheduler.
:param ui_screen: Screen associated with this input manager.
:type ui_screen: The `simpleline.render.screen.UIScreen` based instance.
"""
super().__init__()
self._ui_screen = ui_screen
self._input_error_counter = 0
self._input_error_threshold = 5
self._input_args = None
@property
def input_error_counter(self):
"""Return how many times the user provided bad input."""
return self._input_error_counter
@property
def input_error_threshold_exceeded(self):
"""Did the error counter pass the threshold?
The screen should be redraw.
"""
errors = self._input_error_counter % self._input_error_threshold
return errors == 0
def get_input_blocking(self, message, hidden):
"""Get blocking input from the user.
:param message: Message prompt for the user.
:type message: str
:param hidden: Do not echo user input (password typing).
:type hidden: bool
"""
if hidden:
handler = PasswordInputHandler(source=self)
if self._ui_screen.password_func:
handler.set_pass_func(self._ui_screen.password_func)
else:
handler = InputHandler(source=self)
handler.get_input(message)
handler.wait_on_input()
return handler.value
def get_input(self, args=None):
"""Get input from user.
:param args: Arguments passed in when UIScreen was scheduled.
:type args: Anything.
"""
prompt = self._ui_screen.prompt(args)
if not self._is_input_expected(prompt):
return
self._input_args = args
if not self._ui_screen.hide_user_input:
handler = InputHandler(source=self._ui_screen)
else:
handler = PasswordInputHandler(source=self._ui_screen)
if self._ui_screen.password_func:
handler.set_pass_func(self._ui_screen.password_func)
handler.set_callback(self.process_input)
handler.get_input(prompt)
def _is_input_expected(self, prompt):
"""Check if user handled input processing some other way.
Do nothing if user did handled user input.
:returns: True if prompt is set and we can use it to get user input.
False if prompt is not available, which means that user handled input on their
own.
"""
# None means prompt handled the input by itself -> continue
if prompt is None:
self._input_error_counter = 0
return False
return True
def process_input(self, user_input):
"""Process input from the screens.
:param user_input: User input string.
:type user_input: String.
:raises: ExitMainLoop or any other kind of exception from screen processing.
"""
# process the input, if it wasn't processed (valid)
# increment the error counter
try:
result = self._process_input(user_input)
except ExitMainLoop: # pylint: disable=try-except-raise
raise
except Exception: # pylint: disable=broad-except
App.get_event_loop().enqueue_signal(ExceptionSignal(self))
return
if result.was_successful():
self._input_error_counter = 0
else:
self._input_error_counter += 1
App.get_scheduler().process_input_result(result, self.input_error_threshold_exceeded)
def _process_input(self, key):
"""Method called internally to process unhandled input key presses.
:param key: The string entered by user.
:type key: String.
:return: Return state result object.
:rtype: `simpleline.render.in_out_manager.UserInputResult` class.
:raises: Anything the Screen can raise in the input processing.
"""
from simpleline.render.screen import InputState # pylint: disable=import-outside-toplevel
# delegate the handling to active screen first
key = self._ui_screen.input(self._input_args, key)
if key == InputState.PROCESSED:
return UserInputAction.NOOP
if key == InputState.PROCESSED_AND_REDRAW:
return UserInputAction.REDRAW
if key == InputState.PROCESSED_AND_CLOSE:
return UserInputAction.CLOSE
if key == InputState.DISCARDED:
return UserInputAction.INPUT_ERROR
# global refresh command
if key == Prompt.REFRESH:
return UserInputAction.REDRAW
# global close command
if key == Prompt.CONTINUE:
return UserInputAction.CLOSE
# global quit command
if key == Prompt.QUIT:
return UserInputAction.QUIT
if key is None:
log.warning("Returned key from screen is None. "
"This could be missing return in a screen input method?")
return UserInputAction.INPUT_ERROR
class UserInputAction(Enum):
"""Store user input result."""
INPUT_ERROR = -1
NOOP = 0
REDRAW = 5
CLOSE = 6
QUIT = 7
def was_successful(self):
return self != UserInputAction.INPUT_ERROR
@@ -0,0 +1,94 @@
# Signal handler is giving ability connect and emit to all widgets.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline import App
from simpleline.event_loop.signals import RenderScreenSignal, CloseScreenSignal
class SignalHandler():
"""Provides methods for handling signals anc callbacks.
Provides main methods:
`connect()` -- connect this widget on given signal
`create_signal()` -- create signal class which can be used in the emit method
`emit()` -- emit signal given signal
"""
def connect(self, signal, callback, data=None):
"""Connect this class method with given signal.
:param signal: signal class which you want to connect
:type signal: class based on `simpleline.event_loop.AbstractSignal`
:param callback: the callback function
:type callback: func(event_message, data)
:param data: Data you want to pass to the callback
:type data: Anything
"""
App.get_event_loop().register_signal_handler(signal, callback, data)
def create_signal(self, signal_class, priority=0):
"""Create signal instance usable in the emit method.
:param signal_class: signal you want to use
:type signal_class: class based on `simpleline.event_loop.AbstractSignal`
:param priority: priority of the signal; please look on the
`simpleline.event_loop.AbstractSignal.priority` for further info
:type priority: int
"""
return signal_class(self, priority)
def emit(self, signal):
"""Emit the signal.
This will add `signal` to the event loop.
:param signal: signal to emit
:type signal: instance of class based on `simpleline.event_loop.AbstractSignal`
"""
App.get_event_loop().enqueue_signal(signal)
def create_and_emit(self, signal):
"""Create the signal and emit it.
This is basically shortcut for calling `self.create_signal` and `self.emit`.
"""
created_signal = self.create_signal(signal)
self.emit(created_signal)
def redraw(self):
"""Emit signal to initiate draw.
Add RenderScreenSignal to the event loop.
"""
signal = self.create_signal(RenderScreenSignal)
App.get_event_loop().enqueue_signal(signal)
def close(self):
"""Emit signal to close this screen.
Add CloseScreenSignal to the event loop.
"""
signal = self.create_signal(CloseScreenSignal)
App.get_event_loop().enqueue_signal(signal)
@@ -0,0 +1,58 @@
# Serves shortcuts for easy screen scheduling.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline import App
class ScreenHandler():
@classmethod
def schedule_screen(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.schedule_screen()`.
"""
App.get_scheduler().schedule_screen(ui_screen=ui_screen, args=args)
@classmethod
def replace_screen(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.replace_screen()`.
"""
App.get_scheduler().replace_screen(ui_screen=ui_screen, args=args)
@classmethod
def push_screen(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.push_screen()`.
"""
App.get_scheduler().push_screen(ui_screen=ui_screen, args=args)
@classmethod
def push_screen_modal(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.push_screen_modal()`.
"""
App.get_scheduler().push_screen_modal(ui_screen=ui_screen, args=args)
@@ -0,0 +1,311 @@
# Class handling rendering of the screens to console.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
import threading
from simpleline import App
from simpleline.event_loop import ExitMainLoop
from simpleline.event_loop.signals import ExceptionSignal, RenderScreenSignal, CloseScreenSignal
from simpleline.render import RenderUnexpectedError
from simpleline.render.screen.input_manager import UserInputAction
from simpleline.render.screen_stack import ScreenStack, ScreenData, ScreenStackEmptyException
from simpleline.logging import get_simpleline_logger
log = get_simpleline_logger()
RAW_INPUT_LOCK = threading.Lock()
__all__ = ["ScreenScheduler"]
class ScreenScheduler():
def __init__(self, event_loop, scheduler_stack=None):
"""Constructor where you can pass your own scheduler stack.
The ScreenStack will be used automatically if scheduler stack will be None.
:param event_loop: Event loop used for the scheduler.
:type event_loop: Class based on `simpleline.event_loop.AbstractEventLoop`.
:param scheduler_stack: Use custom scheduler stack if you need to.
:type scheduler_stack: `simpleline.screen_stack.ScreenStack` based class.
"""
self._quit_screen = None
self._event_loop = event_loop
if scheduler_stack:
self._screen_stack = scheduler_stack
else:
self._screen_stack = ScreenStack()
self._register_handlers()
self._first_screen_scheduled = False
@staticmethod
def _spacer():
return "\n".join(2 * [App.get_configuration().width * "="])
def _register_handlers(self):
self._event_loop.register_signal_handler(RenderScreenSignal, self._process_screen_callback)
self._event_loop.register_signal_handler(CloseScreenSignal, self._close_screen_callback)
@property
def quit_screen(self):
"""Return quit UIScreen."""
return self._quit_screen
@quit_screen.setter
def quit_screen(self, quit_screen):
"""Set the UIScreen based instance which will be showed before the Application will quit.
You can also use `simpleline.render.adv_widgets.YesNoDialog` or `UIScreen` based class
with the `answer` property. Without the `answer` property the application will always
close.
"""
self._quit_screen = quit_screen
@property
def nothing_to_render(self):
"""Is something for rendering in the scheduler stack?
:return: True if the rendering stack is empty
:rtype: bool
"""
return self._screen_stack.empty()
def dump_stack(self):
"""Get string representation of actual screen stack."""
return self._screen_stack.dump_stack()
def schedule_screen(self, ui_screen, args=None):
"""Add screen to the bottom of the stack.
This is mostly useful at the beginning to prepare the first screen hierarchy to display.
:param ui_screen: screen to show
:type ui_screen: UIScreen instance
:param args: optional argument, please see switch_screen for details
:type args: anything
"""
log.debug("Scheduling screen %s", ui_screen)
screen = ScreenData(ui_screen, args)
self._screen_stack.add_first(screen)
self._redraw_on_first_scheduled_screen()
def _redraw_on_first_scheduled_screen(self):
if not self._first_screen_scheduled:
self.redraw()
self._first_screen_scheduled = True
def replace_screen(self, ui_screen, args=None):
"""Schedules a screen to replace the current one.
:param ui_screen: screen to show
:type ui_screen: instance of UIScreen
:param args: optional argument to pass to ui's refresh and setup methods
(can be used to select what item should be displayed or so)
:type args: anything
"""
log.debug("Replacing screen %s", ui_screen)
try:
execute_new_loop = self._screen_stack.pop().execute_new_loop
except ScreenStackEmptyException as e:
raise ScreenStackEmptyException("Switch screen is not possible when there is no "
"screen scheduled!") from e
# we have to keep the old_loop value so we stop
# dialog's mainloop if it ever uses switch_screen
screen = ScreenData(ui_screen, args, execute_new_loop)
self._screen_stack.append(screen)
self.redraw()
def push_screen(self, ui_screen, args=None):
"""Schedules a screen to show, but keeps the current one in stack to
return to, when the new one is closed.
:param ui_screen: screen to show
:type ui_screen: UIScreen instance
:param args: optional argument
:type args: anything
"""
log.debug("Pushing screen %s to stack", ui_screen)
screen = ScreenData(ui_screen, args, False)
self._screen_stack.append(screen)
self.redraw()
def push_screen_modal(self, ui_screen, args=None):
"""Starts a new screen right away, so the caller can collect data back.
When the new screen is closed, the caller is redisplayed.
This method does not return until the new screen is closed.
:param ui_screen: screen to show
:type ui_screen: UIScreen instance
:param args: optional argument, please see switch_screen for details
:type args: anything
"""
log.debug("Pushing modal screen %s to stack", ui_screen)
screen = ScreenData(ui_screen, args, True)
self._screen_stack.append(screen)
# only new events will be processed now
# the old one will wait after this event loop will be closed
self._event_loop.execute_new_loop(RenderScreenSignal(self))
def _close_screen_callback(self, signal, data):
self.close_screen(signal.source)
def close_screen(self, closed_from=None):
"""Close the currently displayed screen and exit it's main loop if necessary.
Next screen from the stack is then displayed.
"""
screen = self._screen_stack.pop()
log.debug("Closing screen %s from %s", screen, closed_from)
# User can react when screen is closing
screen.ui_screen.closed()
if closed_from is not None and closed_from is not screen.ui_screen:
raise RenderUnexpectedError("You are trying to close screen %s from screen %s! "
"This is most probably not intentional." %
(closed_from, screen.ui_screen))
if screen.execute_new_loop:
self._event_loop.close_loop()
# redraw screen if there is what to redraw
# and if it is not modal screen (modal screen parent is blocked)
if not self._screen_stack.empty() and not screen.execute_new_loop:
self.redraw()
# we can't draw anything more. Kill the application.
if self._screen_stack.empty():
raise ExitMainLoop()
def redraw(self):
"""Register rendering to the event loop for processing."""
self._event_loop.enqueue_signal(RenderScreenSignal(self))
def _process_screen_callback(self, signal, data):
self._process_screen()
def _process_screen(self):
"""Process the current screen.
1) It will call setup if the screen is not already set.
2a) If setup was success then draw the screen.
2b) If setup wasn't successful then pop the screen and try to process next in the stack.
Continue by (1).
3)Ask for user input if requested.
"""
top_screen = self._get_last_screen()
log.debug("Processing screen %s", top_screen)
# this screen is used first time (call setup() method)
if not top_screen.ui_screen.screen_ready:
if not top_screen.ui_screen.setup(top_screen.args):
# remove the screen and skip if setup went wrong
self._screen_stack.pop()
self.redraw()
log.warning("Screen %s setup wasn't successful", top_screen)
return
# get the widget tree from the screen and show it in the screen
try:
# refresh screen content
top_screen.ui_screen.refresh(top_screen.args)
# Screen was closed in the refresh method
if top_screen != self._get_last_screen():
return
# draw screen to the console
self._draw_screen(top_screen)
if top_screen.ui_screen.input_required:
log.debug("Input is required by %s screen", top_screen)
top_screen.ui_screen.get_input_with_error_check(top_screen.args)
except ExitMainLoop: # pylint: disable=try-except-raise
raise
except Exception: # pylint: disable=broad-except
self._event_loop.enqueue_signal(ExceptionSignal(self))
return
def _draw_screen(self, active_screen):
"""Draws the current `active_screen`.
:param active_screen: Screen which should be draw to the console.
:type active_screen: Classed based on `simpleline.render.screen.UIScreen`.
"""
# get the widget tree from the screen and show it in the screen
try:
if not active_screen.ui_screen.no_separator:
# separate the content on the screen from the stuff we are about to display now
print(self._spacer())
# print UIScreen content
active_screen.ui_screen.show_all()
except ExitMainLoop: # pylint: disable=try-except-raise
raise
except Exception: # pylint: disable=broad-except
self._event_loop.enqueue_signal(ExceptionSignal(self))
def _get_last_screen(self):
if self._screen_stack.empty():
raise ExitMainLoop()
return self._screen_stack.pop(False)
def process_input_result(self, input_result, should_redraw):
active_screen = self._get_last_screen()
if not input_result.was_successful():
if should_redraw:
self.redraw()
else:
log.debug("Input was not successful, ask for new input.")
active_screen.ui_screen.get_input_with_error_check(active_screen.args)
else:
if input_result == UserInputAction.NOOP:
return
if input_result == UserInputAction.REDRAW:
self.redraw()
elif input_result == UserInputAction.CLOSE:
self.close_screen()
elif input_result == UserInputAction.QUIT:
if self.quit_screen:
self.push_screen_modal(self.quit_screen)
try:
if self.quit_screen.answer is True:
raise ExitMainLoop()
self.redraw()
except AttributeError as e:
raise ExitMainLoop() from e
else:
raise ExitMainLoop()
@@ -0,0 +1,117 @@
# Classes implementation for storing and manipulating Screen stack.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline.errors import SimplelineError
class ScreenStackException(SimplelineError):
"""General screen stack exception."""
class ScreenStackEmptyException(ScreenStackException):
"""Screen stack exception when stack is empty."""
class ScreenStack():
"""Managing screen stack used in `ScreenScheduler`."""
def __init__(self):
self._screens = []
def empty(self):
"""Test if screen stack is empty.
:return: True if empty.
:rtype: bool
"""
return not self._screens
def size(self):
"""Get size of the stack.
:return: Size of the stack.
"""
return len(self._screens)
def append(self, screen):
"""Add new screen to the top of the stack.
:param screen: Screen for the future rendering.
:type screen: Class based on `simpleline.render.ui_screen.UIScreen`.
"""
self._screens.append(screen)
def pop(self, remove=True):
"""Return top item from the stack.
:param remove: If True (default) also remove this items from the stack.
:return: The top screen on the stack.
"""
try:
if remove:
return self._screens.pop()
return self._screens[-1]
except IndexError as e:
raise ScreenStackEmptyException(e) from e
def add_first(self, screen):
"""Add `screen` to the bottom of the stack.
:param screen: Add the `screen` to the bottom of the stack.
:type screen: Class based on `simpleline.render.ui_screen.UIScreen`.
"""
self._screens.insert(0, screen)
def dump_stack(self):
"""Dump screen stack structure.
:returns: Screen stack representation.
:rtype: str
"""
msg = '======= Screen stack =======\n'
msg += '----------- TOP ------------\n'
for screen in reversed(self._screens):
msg += str(screen)
msg += "\n"
msg += '============================\n'
return msg
class ScreenData():
"""Inner data class to store screen data."""
def __init__(self, ui_screen, args=None, execute_new_loop=False):
self.ui_screen = ui_screen
self.args = args
self.execute_new_loop = execute_new_loop
def __str__(self):
msg = self.__class__.__name__
msg += "("
msg += ",".join((str(self.ui_screen), str(self.args), str(self.execute_new_loop)))
msg += ")"
return msg
@@ -0,0 +1,495 @@
# Widgets for Text UI framework.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
import functools
from textwrap import wrap
from simpleline.utils.i18n import _
from simpleline.utils import ensure_str
__all__ = ["Widget", "TextWidget", "SeparatorWidget", "EntryWidget", "ColumnWidget",
"CheckboxWidget", "CenterWidget"]
class Widget():
def __init__(self, max_width=None, default=None):
"""Initializes base Widgets buffer.
This class can be subclassed to create customized widgets.
:param max_width: serves as a hint about screen size to write method with default arguments
:type max_width: int
:param default: string containing the default content to fill the buffer with
:type default: string
"""
self._buffer = []
if default:
self._buffer = [[c for c in l] for l in default.split("\n")] # pylint: disable=unnecessary-comprehension
self._max_width = max_width
self._cursor = (0, 0) # row, col
@property
def height(self):
"""The current height of the internal buffer."""
return len(self._buffer)
@property
def width(self):
"""The current width of the internal buffer (id of the first empty column)."""
return functools.reduce(lambda acc, l: max(acc, len(l)), self._buffer, 0)
def clear(self):
"""Clears this widgets buffer and resets cursor."""
self._buffer = list()
self._cursor = (0, 0)
@property
def content(self):
"""Return a list (rows) of lists (columns) with one character elements."""
return self._buffer
def render(self, width):
"""Redraw the widget's self._buffer.
:param width: the width of buffer requested by the caller
:type width: int
Commonly, call render of child widgets and then draw and write
methods to copy their contents to self._buffer.
"""
self.clear()
def get_lines(self):
"""Return lines to write out in order to show this widget.
:return: lines representing this widget
:rtype: list(str)
"""
return [str(u"".join(line)) for line in self._buffer]
def set_cursor_position(self, row, col):
"""Set cursor position.
:param row: row id, starts with 0 at the top of the screen
:type row: int
:param col: column id, starts with 0 on the left side of the screen
:type col: int
"""
self._cursor = (row, col)
@property
def cursor(self):
return self._cursor
def set_end(self):
"""Set the cursor to first column in new line at the end."""
self._cursor = (self.height, 0)
def draw(self, w, row=None, col=None, block=False):
"""Copy w widget's content to this widget's buffer at row, col position.
:param w: widget to take content from
:type w: class Widget
:param row: row number to start at (default is at the cursor position)
:type row: int
:param col: column number to start at (default is at the cursor position)
:type col: int
:param block: when printing newline, start at column col (True) or at column 0 (False)
:type block: boolean
"""
# if the starting row is not present, start at the cursor position
if row is None:
row = self._cursor[0]
# if the starting column is not present, start at the cursor position
if col is None:
col = self._cursor[1]
# fill up rows to accommodate for w.height
if self.height < row + w.height:
for _i in range(row + w.height - self.height):
self._buffer.append(list())
# append columns to accommodate for w.width
for l in range(row, row + w.height):
l_len = len(self._buffer[l])
w_len = len(w.content[l - row])
if l_len < col + w_len:
self._buffer[l] += ((col + w_len - l_len) * list(u" "))
self._buffer[l][col:col + w_len] = w.content[l - row][:]
# move the cursor to new spot
if block:
self._cursor = (row + w.height, col)
else:
self._cursor = (row + w.height, 0)
def write(self, text, row=None, col=None, width=None, block=False, wordwrap=False):
"""Emulate the typing machine writing to this widget's buffer.
:param text: text to type
:type text: str
:param row: row number to start at (default is at the cursor position)
:type row: int
:param col: column number to start at (default is at the cursor position)
:type col: int
:param width: wrap at "col" + "width" column (default is at self._max_width)
:type width: int
:param block: when printing newline, start at column col (True) or at column 0 (False)
:type block: boolean
:param wordwrap: wrap by words
:type wordwrap: boolean
"""
if not text:
return
text = ensure_str(text)
if row is None:
row = self._cursor[0]
if col is None:
col = self._cursor[1]
if width is None and self._max_width:
width = self._max_width - col
x = row
y = col
if wordwrap:
text = self._wrap_words(text, width)
# emulate typing machine
for character in text:
# FIXME: Remove the code duplication below and optimize it
# process newline
if character == "\n":
x += 1
if block:
y = col
else:
y = 0
self._increase_x_buffer_size(x)
continue
self._increase_x_buffer_size(x)
self._increase_y_buffer_size(x, y)
self._save_character_to_buffer(x, y, character)
# shift to the next char
y += 1
if width is not None and y >= col + width:
x += 1
if block:
y = col
else:
y = 0
self._cursor = (x, y)
def _increase_x_buffer_size(self, x):
if x >= len(self._buffer):
for _i in range(x - len(self._buffer) + 1):
self._buffer.append(list())
def _increase_y_buffer_size(self, x, y):
if y >= len(self._buffer[x]):
self._buffer[x] += ((y - len(self._buffer[x]) + 1) * list(u" "))
def _save_character_to_buffer(self, x, y, character):
self._buffer[x][y] = character
@staticmethod
def _wrap_words(text, width):
lines = []
# Wrap each line separately
for line in text.split('\n'):
sublines = []
for subline in wrap(line, width):
sublines.append(subline)
if len(subline) < width:
# line shorter than width will be wrapped by '\n' we add
sublines.append('\n')
# line with length == width will be wrapped by the width based
# wrapping logic
# end of line will be wrapped by '\n' following the line in
# original text
if sublines and sublines[-1] == '\n':
sublines.pop()
lines.append("".join(sublines))
return '\n'.join(lines)
class TextWidget(Widget):
"""Class to handle wrapped text output."""
def __init__(self, text):
"""
:param text: text to format
:type text: str
"""
super().__init__()
self._text = text
@property
def text(self):
"""Contains text of this widget."""
return self._text
def render(self, width):
"""Renders the text widget limited to width number of columns.
Wraps to the next line when the text is longer.
:param width: maximum width allocated to the string
:type width: int
"""
super().render(width)
self.write(self._text, width=width, wordwrap=True)
class EntryWidget(TextWidget):
"""This is the easy way how to generate entry items for containers.
If the numbering in a container is turned on the output looks like:
N) title
value
Without numbering turned on:
title
value
"""
def __init__(self, title, value=None):
""" Create Entry widget instance.
:param title: Title of this entry.
:type title: String.
:param value: Actual value printed in second line below the title.
:type value: String.
"""
text = self._create_text(title=title, value=value)
super().__init__(text)
@staticmethod
def _create_text(title, value):
msg = title
if value:
msg += "\n"
msg += value
return msg
class SeparatorWidget(Widget):
"""Print empty line."""
def __init__(self, lines=1):
"""Construct SeparatorWidget for printing blank lines.
:param lines: How many lines should be blank.
:type lines: int greater than 0.
"""
super().__init__()
self._lines = lines
def render(self, width):
"""Render empty line to the buffer.
:param width: maximum width allocated to the string
:type width: int
"""
super().render(width)
self.write("")
def write(self, text, row=None, col=None, width=None, block=False, wordwrap=False):
"""Optimize write function.
To print just a blank line we don't need too much logic.
"""
for i in range(0, self._lines):
self._buffer.append(list())
self._buffer[i] += u""
self.set_cursor_position(self._lines - 1, 0)
class CenterWidget(Widget):
"""Class to handle horizontal centering of content."""
def __init__(self, w):
"""
:param w: widget to center
:type w: Widget
"""
super().__init__()
self._w = w
def render(self, width):
"""Render the centered widget to internal buffer.
:param width: maximum width the widget should use
:type width: int
"""
super().render(width)
self._w.render(width)
# make sure col is an integer
self.draw(self._w, col=(width - self._w.width) // 2)
class CheckboxWidget(Widget):
"""Widget to show checkbox with (un)checked box, name and description."""
def __init__(self, key="x", title=None, text=None, completed=None):
"""
:param key: tick character to be used inside [ ]
:type key: character
:param title: the title next to the [ ] box
:type title: str
:param text: the description text to be shown on the second row in ()
:type text: str
:param completed: is the checkbox ticked or not?
:type completed: True|False
"""
super().__init__()
self._key = key
self._title = title
self._text = text
self._completed = completed
def render(self, width):
"""Render the widget to internal buffer.
It should be max width characters wide.
"""
super().render(width)
if self.completed:
checkchar = self._key
else:
checkchar = " "
# prepare the checkbox
checkbox = TextWidget("[%s]" % checkchar)
data = []
# append lines
if self.title:
data.append(TextWidget(_(self.title)))
if self.text:
data.append(TextWidget("(%s)" % self.text))
# the checkbox has two columns
# [x] is one and is 3 chars wide
# text is second and can occupy width - 3 - 1 (for space) chars
cols = ColumnWidget([(3, [checkbox]), (width - 4, data)], 1)
cols.render(width)
# transfer the column widget rendered stuff to internal buffer
self.draw(cols)
@property
def title(self):
"""Returns the first line (main title) of the checkbox."""
return self._title
@property
def completed(self):
"""Returns the state of the checkbox, checked is True."""
return self._completed
@property
def text(self):
"""Contains the description text from the second line."""
return self._text
class ColumnWidget(Widget):
def __init__(self, columns, spacing=0):
"""Create text columns
Deprecated. Please do not use this widget, use containers instead.
:param columns: list containing (column width, [list of widgets to put into this column])
:type columns: [(int, [...]), ...]
:param spacing: number of spaces to use between columns
:type spacing: int
"""
super().__init__()
self._spacing = spacing
self._columns = columns
def render(self, width):
"""Render the widget to it's internal buffer
:param width: the maximum width the widget can use
:type width: int
:return: nothing
"""
super().render(width)
# the leftmost empty column
col_pos = 0
# iterate over tuples (column width, column content)
for col_width, col in self._columns:
# set cursor to first line and leftmost empty column
self.set_cursor_position(0, col_pos)
# if requested width is None, limit the maximum to width
# and set minimum to 0
if col_width is None:
col_max_width = width - self.cursor[1]
col_width = 0
else:
col_max_width = col_width
# render and draw contents of column
for item in col:
item.render(col_max_width)
self.draw(item, block=True)
# recompute the leftmost empty column
col_pos = max((col_pos + col_width), self.width) + self._spacing