feat(Anaconda): Local Repo
This commit is contained in:
+31
@@ -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."""
|
||||
+253
@@ -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
|
||||
+475
@@ -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
|
||||
+154
@@ -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) + ": "
|
||||
+313
@@ -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
|
||||
+203
@@ -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
|
||||
+94
@@ -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)
|
||||
+58
@@ -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)
|
||||
+311
@@ -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()
|
||||
+117
@@ -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
|
||||
+495
@@ -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
|
||||
Reference in New Issue
Block a user