feat(Anaconda): Local Repo
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
# Base class for Simpleline Text UI framework.
|
||||
#
|
||||
# Library containing the 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/>.
|
||||
#
|
||||
|
||||
__all__ = ["App"]
|
||||
|
||||
|
||||
from simpleline.logging import setup_logging
|
||||
from simpleline.errors import NothingScheduledError
|
||||
|
||||
setup_logging()
|
||||
|
||||
|
||||
class App():
|
||||
"""This is the main class for Simpleline library.
|
||||
|
||||
Do not create instance of this class. Use this class as static!
|
||||
The `initialize()` method must be called before use.
|
||||
|
||||
It is giving you access to the scheduler and event loop. You can have only one instance of this
|
||||
class in your application.
|
||||
|
||||
To create this instance call `App.initialize()` method. This method can also be used to
|
||||
reset settings in the App class to start with new event loop or scheduler.
|
||||
"""
|
||||
__app = None
|
||||
|
||||
class AppPimpl():
|
||||
|
||||
def __init__(self, scheduler, event_loop, configuration):
|
||||
self.event_loop = event_loop
|
||||
self.scheduler = scheduler
|
||||
self.configuration = configuration
|
||||
|
||||
@classmethod
|
||||
def initialize(cls, scheduler=None, event_loop=None, global_configuration=None):
|
||||
"""Create app instance inside of this class.
|
||||
|
||||
This method can be called multiple times to reset App settings.
|
||||
|
||||
:param scheduler: scheduler used for rendering screens; if not specified use
|
||||
`simpleline.render.screen_scheduler.ScreenScheduler`.
|
||||
:type scheduler: instance of `simpleline.render.screen_scheduler.ScreenScheduler`.
|
||||
|
||||
:param event_loop: event loop used for asynchronous tasks;
|
||||
if not specified use `simpleline.event_loop.main_loop.MainLoop`.
|
||||
:type event_loop: object based on class `simpleline.event_loop.AbstractEventLoop`.
|
||||
|
||||
:param global_configuration: instance of the global configuration object; if not specified
|
||||
use `simpleline.global_configuration.GlobalConfiguration`
|
||||
:type global_configuration: object based on class
|
||||
`simpleline.global_configuration.GlobalConfiguration`
|
||||
"""
|
||||
from simpleline.event_loop.main_loop import MainLoop # pylint: disable=import-outside-toplevel
|
||||
from simpleline.render.screen_scheduler import ScreenScheduler # pylint: disable=import-outside-toplevel
|
||||
from simpleline.global_configuration import GlobalConfiguration # pylint: disable=import-outside-toplevel
|
||||
|
||||
if event_loop is None:
|
||||
event_loop = MainLoop()
|
||||
if scheduler is None:
|
||||
scheduler = ScreenScheduler(event_loop)
|
||||
if global_configuration is None:
|
||||
global_configuration = GlobalConfiguration()
|
||||
|
||||
cls.__app = cls.AppPimpl(scheduler, event_loop, global_configuration)
|
||||
|
||||
cls._post_init()
|
||||
|
||||
@classmethod
|
||||
def _post_init(cls):
|
||||
from simpleline.input.input_threading import InputThreadManager # pylint: disable=import-outside-toplevel
|
||||
# FIXME: This should be done by more general way not by calling exact class here.
|
||||
# Create new instance of InputThreadManager because it needs new event loop
|
||||
InputThreadManager.create_new_instance()
|
||||
|
||||
@classmethod
|
||||
def is_initialized(cls):
|
||||
"""Is the App already initialized?
|
||||
|
||||
:returns: True if the `App.initialized()` method was called, False otherwise.
|
||||
"""
|
||||
if cls.__app is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_scheduler(cls):
|
||||
"""Get instance of class responsible for rendering of the screen."""
|
||||
return cls.__app.scheduler
|
||||
|
||||
@classmethod
|
||||
def get_event_loop(cls):
|
||||
"""Get instance of class responsible for processing asynchronous events."""
|
||||
return cls.__app.event_loop
|
||||
|
||||
@classmethod
|
||||
def get_configuration(cls):
|
||||
"""Get application defaults configuration object."""
|
||||
return cls.__app.configuration
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
"""Run event loop.
|
||||
|
||||
Raise an exception if no screen is scheduled. This behavior can be changed by
|
||||
`should_run_with_empty_stack` global configuration option.
|
||||
|
||||
This is shortcut to `App.event_loop().run()`.
|
||||
:raises NothingScheduledError: when there is no screen scheduled
|
||||
"""
|
||||
if not cls.__app.configuration.should_run_with_empty_stack:
|
||||
# Check if the screen stack is not empty
|
||||
if cls.__app.scheduler.nothing_to_render:
|
||||
raise NothingScheduledError("Can't run application with the empty screen stack! "
|
||||
"To avoid this please see should_run_with_empty_stack "
|
||||
"global configuration option.")
|
||||
App.get_event_loop().run()
|
||||
@@ -0,0 +1,31 @@
|
||||
# Base exceptions for the Simpleline application.
|
||||
#
|
||||
# Base class for Simpleline 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/>.
|
||||
#
|
||||
# This can't be moved to __init__.py because of cyclic imports error.
|
||||
#
|
||||
|
||||
|
||||
class SimplelineError(Exception):
|
||||
"""Base exception for all other exceptions."""
|
||||
|
||||
|
||||
class NothingScheduledError(SimplelineError):
|
||||
"""Exception when running the loop with no screens scheduled."""
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
# Abstract base class for Simpleline Event Loop.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# This class can be overridden to use any existing event loop of your program.
|
||||
#
|
||||
# Author(s): Jiri Konecny <jkonecny@redhat.com>
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from collections import namedtuple
|
||||
|
||||
from simpleline.errors import SimplelineError
|
||||
from simpleline.event_loop.ticket_machine import TicketMachine
|
||||
from simpleline.logging import get_simpleline_logger
|
||||
|
||||
log = get_simpleline_logger()
|
||||
|
||||
__all__ = ["AbstractEventLoop", "AbstractSignal", "ExitMainLoop"]
|
||||
|
||||
QuitCallback = namedtuple("QuitCallback", ["callback", "args"])
|
||||
|
||||
|
||||
class ExitMainLoop(SimplelineError):
|
||||
"""This exception ends the whole event loop."""
|
||||
|
||||
|
||||
class AbstractEventLoop(metaclass=ABCMeta):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._handlers = {}
|
||||
self._processed_signals = TicketMachine()
|
||||
self._quit_callback = None
|
||||
# end most inner loop politely by setting to False
|
||||
self._run_loop = True
|
||||
self._force_quit = False
|
||||
|
||||
def register_signal_handler(self, signal, callback, data=None):
|
||||
"""Register a callback which will be called when message "event"
|
||||
is encountered during process_events.
|
||||
|
||||
The callback has to accept two arguments:
|
||||
- the received message in the form of (type, [arguments])
|
||||
- the data registered with the handler
|
||||
|
||||
:param signal: Signal class we want to react on.
|
||||
:type signal: Class based on the simpleline.event_loop.AbstractSignal class.
|
||||
|
||||
:param callback: The callback function.
|
||||
:type callback: func(event_message, data)
|
||||
|
||||
:param data: Optional data to pass to callback.
|
||||
:type data: Anything.
|
||||
"""
|
||||
if signal not in self._handlers:
|
||||
self._handlers[signal] = []
|
||||
|
||||
event_handler = self._create_event_handler(callback, data)
|
||||
self._handlers[signal].append(event_handler)
|
||||
|
||||
@abstractmethod
|
||||
def register_signal_source(self, signal_source):
|
||||
"""Register source of signal for actual event queue.
|
||||
|
||||
:param signal_source: Source for future signals.
|
||||
:type signal_source: `simpleline.render.ui_screen.UIScreen`
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def enqueue_signal(self, signal):
|
||||
"""Enqueue new event for processing.
|
||||
|
||||
:param signal: Signal which you want to add to the event queue for processing.
|
||||
:type signal: Instance based on AbstractEvent class.
|
||||
"""
|
||||
log.debug("New signal %s enqueued with source %s",
|
||||
signal,
|
||||
signal.source.__class__.__name__)
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
"""Starts the event loop."""
|
||||
log.debug("Starting main loop")
|
||||
self._force_quit = False
|
||||
|
||||
def force_quit(self):
|
||||
"""Force quit all running event loops.
|
||||
|
||||
Kill all loop including inner loops (modal window).
|
||||
None of the Simpleline events will be processed anymore.
|
||||
"""
|
||||
log.debug("Force quit called. Killing all loops!")
|
||||
self._force_quit = True
|
||||
|
||||
@abstractmethod
|
||||
def execute_new_loop(self, signal):
|
||||
"""Starts the new event loop and pass `signal` in it.
|
||||
|
||||
This is required for processing a modal screens.
|
||||
|
||||
:param signal: Signal passed to the new event loop.
|
||||
:type signal: The `AbstractSignal` based class.
|
||||
"""
|
||||
log.debug("Executing inner loop")
|
||||
|
||||
@abstractmethod
|
||||
def close_loop(self):
|
||||
"""Close active event loop.
|
||||
|
||||
Close an event loop created by the `execute_new_loop()` method.
|
||||
"""
|
||||
log.debug("Closing inner loop")
|
||||
|
||||
@abstractmethod
|
||||
def process_signals(self, return_after=None):
|
||||
"""This method processes incoming async messages.
|
||||
|
||||
Process signals enqueued by the `self.enqueue_signal()` method. Call handlers
|
||||
registered to the signals by the `self.register_signal_handler()` method.
|
||||
|
||||
When `return_after` is specified then wait to the point when this signal is processed.
|
||||
NO warranty that this method will return immediately after the signal was processed!
|
||||
|
||||
Without `return_after` parameter this method will return after all queued signals
|
||||
with the highest priority will be processed.
|
||||
|
||||
The method is NOT thread safe!
|
||||
|
||||
:param return_after: Wait on this signal to be processed.
|
||||
:type return_after: Class of the signal.
|
||||
"""
|
||||
|
||||
def set_quit_callback(self, callback, args=None):
|
||||
"""Call this callback when event loop quits.
|
||||
|
||||
:param callback: Call this callback when event loops ends (application quit).
|
||||
:type callback: Function with one parameter data `func(data)`.
|
||||
|
||||
:param args: Arguments passed to the quit callback.
|
||||
:type args: Anything.
|
||||
"""
|
||||
self._quit_callback = QuitCallback(callback, args)
|
||||
|
||||
def kill_app_with_traceback(self, exception_signal, data=None):
|
||||
"""Print exception and screen stack and kill the application.
|
||||
|
||||
:param exception_signal: ExceptionSignal encapsulating the original exception which
|
||||
will be passed to the sys.excepthook method.
|
||||
:type exception_signal: Instance of `simpleline.event_loop.signals.ExceptionSignal` class.
|
||||
|
||||
:param data: To be usable as signal handler.
|
||||
:type data: Anything will be ignored.
|
||||
"""
|
||||
log.debug("Unhandled error in handler raised:")
|
||||
sys.excepthook(*exception_signal.exception_info)
|
||||
|
||||
from simpleline import App # pylint: disable=import-outside-toplevel
|
||||
stack_dump = App.get_scheduler().dump_stack()
|
||||
print("")
|
||||
print(stack_dump)
|
||||
log.error(stack_dump)
|
||||
|
||||
log.debug("Killing application!")
|
||||
sys.exit(1)
|
||||
|
||||
@staticmethod
|
||||
def _create_event_handler(callback, data):
|
||||
"""Create event handler data object and return it."""
|
||||
return EventHandler(callback=callback, data=data)
|
||||
|
||||
def _register_wait_on_signal(self, wait_on_signal):
|
||||
"""Register process waiting on signal `wait_on_signal` and return id for later checking.
|
||||
|
||||
ID is returned which is then used in the `self._check_if_signal_processed()` method.
|
||||
|
||||
:param wait_on_signal: Signal we are waiting for.
|
||||
:type wait_on_signal: Class based on `simpleline.event_loop.AbstractSignal`.
|
||||
"""
|
||||
return self._processed_signals.take_ticket(wait_on_signal.__name__)
|
||||
|
||||
def _mark_signal_processed(self, signal):
|
||||
"""Mark that processes waiting on this signal that they are able to go.
|
||||
|
||||
:param signal: Signal which was processed.
|
||||
:type signal: Class based on `simpleline.event_loop.AbstractSignal`.
|
||||
"""
|
||||
self._processed_signals.mark_line_to_go(signal.__class__.__name__)
|
||||
|
||||
def _check_if_signal_processed(self, wait_on_signal, unique_id):
|
||||
"""Check if the signal was processed.
|
||||
|
||||
:param wait_on_signal: Signal the process is waiting for.
|
||||
:type wait_on_signal: Class based on `simpleline.event_loop.AbstractSignal`.
|
||||
|
||||
:param unique_id: Unique id returned by the `self._register_wait_on_signal()` method.
|
||||
:type unique_id: int
|
||||
"""
|
||||
return self._processed_signals.check_ticket(wait_on_signal.__name__, unique_id)
|
||||
|
||||
|
||||
class EventHandler():
|
||||
"""Data class to save event handlers."""
|
||||
|
||||
def __init__(self, callback, data):
|
||||
self.callback = callback
|
||||
self.data = data
|
||||
|
||||
|
||||
class AbstractSignal(metaclass=ABCMeta):
|
||||
"""This class is base class for signals.
|
||||
|
||||
.. NOTE:
|
||||
Ordering and equality is based on priority.
|
||||
"""
|
||||
|
||||
def __init__(self, source, priority=0):
|
||||
self._source = source
|
||||
self._priority = priority
|
||||
|
||||
def __lt__(self, other):
|
||||
"""Order Signal classes by priority."""
|
||||
return self._priority < other.priority
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Order Signal classes by priority."""
|
||||
return self._priority == other.priority
|
||||
|
||||
def __str__(self):
|
||||
"""For easier logging."""
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def priority(self):
|
||||
"""Priority of this event.
|
||||
|
||||
Values less than 0 denote higher priorities. Values greater than 0 denote lower priorities.
|
||||
Events from high priority sources are always processed before events from lower priority
|
||||
sources.
|
||||
"""
|
||||
return self._priority
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Source which emitted this event."""
|
||||
return self._source
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
# Default event queue for Simpleline application.
|
||||
#
|
||||
# This class is thread safe.
|
||||
#
|
||||
# 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 queue import PriorityQueue
|
||||
from threading import Lock
|
||||
|
||||
from simpleline.errors import SimplelineError
|
||||
|
||||
|
||||
class EventQueueError(SimplelineError):
|
||||
"""Main exception for `EventQueue` class.
|
||||
|
||||
Inherits from `simpleline.SimplelineError`.
|
||||
"""
|
||||
|
||||
|
||||
class EventQueue():
|
||||
"""Class for managing signal queue.
|
||||
|
||||
Responsibilities of this class are:
|
||||
* sorting by priority of signals
|
||||
* managing sources for this event queue
|
||||
* enqueuing signals
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._queue = PriorityQueue()
|
||||
self._contained_screens = set()
|
||||
self._lock = Lock()
|
||||
|
||||
def empty(self):
|
||||
"""Return true if Queue is empty.
|
||||
|
||||
:return: True if empty, False otherwise.
|
||||
"""
|
||||
return self._queue.empty()
|
||||
|
||||
def enqueue(self, signal):
|
||||
"""Enqueue signal to this queue.
|
||||
|
||||
:param signal: Signal which should be enqueued to this queue.
|
||||
:type signal: Signal class based on `simpleline.event_loop.signals.AbstractSignal`.
|
||||
"""
|
||||
self._queue.put(signal)
|
||||
|
||||
def enqueue_if_source_belongs(self, signal, source):
|
||||
"""Enqueue signal to this queue if the signal source belongs to this queue.
|
||||
|
||||
Enqueue the `signal` only if the `source` belongs to this queue.
|
||||
See the `add_source()` method.
|
||||
|
||||
:param signal: Signal which should be enqueued to this queue.
|
||||
:type signal: Signal class based on `simpleline.event_loop.signals.AbstractSignal`.
|
||||
:param source: Source of this signal.
|
||||
:type source: Anything.
|
||||
:return: True if the source belongs to this queue and signal was queued, False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
if self.contains_source(source):
|
||||
self._queue.put(signal)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get(self):
|
||||
"""Return enqueued signal with the highest priority.
|
||||
|
||||
This is FIFO implementation for the same priority.
|
||||
If the queue is empty this method will wait for the input signal.
|
||||
|
||||
:return: Queued signal.
|
||||
:rtype: Signal based on class `simpleline.event_loop.signals.AbstractSignal`.
|
||||
"""
|
||||
return self._queue.get()
|
||||
|
||||
def get_top_event_if_priority(self, priority):
|
||||
"""Return top enqueued signal if priority is equal to `priority`. Otherwise `None`.
|
||||
|
||||
:param priority: Requested event priority.
|
||||
:type priority: int
|
||||
|
||||
:return: Queued signal if it has requested priority. Otherwise `None`.
|
||||
:rtype: Signal based on class `simpleline.event_loop.signals.AbstractSignal` or `None`.
|
||||
"""
|
||||
event = self._queue.get()
|
||||
if event.priority == priority:
|
||||
return event
|
||||
|
||||
self._queue.put(event)
|
||||
return None
|
||||
|
||||
def add_source(self, signal_source):
|
||||
"""Add new source of signals to this queue.
|
||||
|
||||
This method is mandatory for `enqueue_if_source_belongs()` method.
|
||||
The same source will be added only once.
|
||||
|
||||
:param signal_source: Source of future signals.
|
||||
:type signal_source: Anything which will emit signals in future.
|
||||
"""
|
||||
# TODO: Remove when python3-astroid 1.5.3 will be in Fedora
|
||||
# pylint: disable=not-context-manager
|
||||
with self._lock:
|
||||
self._contained_screens.add(signal_source)
|
||||
|
||||
def remove_source(self, signal_source):
|
||||
"""Remove signal source from this queue.
|
||||
|
||||
:param signal_source: Source of future signals.
|
||||
:type signal_source: Anything.
|
||||
:raise: EventQueueError"""
|
||||
try:
|
||||
# TODO: Remove when python3-astroid 1.5.3 will be in Fedora
|
||||
# pylint: disable=not-context-manager
|
||||
with self._lock:
|
||||
self._contained_screens.remove(signal_source)
|
||||
except KeyError as e:
|
||||
raise EventQueueError("Can't remove non-existing event source!") from e
|
||||
|
||||
def contains_source(self, signal_source):
|
||||
"""Test if `signal_source` belongs to this queue.
|
||||
|
||||
:param signal_source: Source of signals.
|
||||
:type signal_source: Anything.
|
||||
:return: True if signal source belongs to this queue.
|
||||
:rtype: bool
|
||||
"""
|
||||
# TODO: Remove when python3-astroid 1.5.3 will be in Fedora
|
||||
# pylint: disable=not-context-manager
|
||||
with self._lock:
|
||||
return signal_source in self._contained_screens
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
# Glib event queue used by Simpleline application.
|
||||
#
|
||||
# This class is thread safe.
|
||||
#
|
||||
# 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 collections import namedtuple
|
||||
|
||||
import gi
|
||||
|
||||
from simpleline.event_loop import AbstractEventLoop, ExitMainLoop
|
||||
from simpleline.event_loop.signals import ExceptionSignal
|
||||
from simpleline.logging import get_simpleline_logger
|
||||
|
||||
gi.require_version("GLib", "2.0")
|
||||
|
||||
from gi.repository import GLib # pylint: disable=wrong-import-order, wrong-import-position
|
||||
|
||||
log = get_simpleline_logger()
|
||||
|
||||
CallbackArgs = namedtuple("CallbackArgs", ["signal", "source", "handlers"])
|
||||
|
||||
|
||||
__all__ = ["GLibEventLoop"]
|
||||
|
||||
|
||||
class GLibEventLoop(AbstractEventLoop):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Create first loop
|
||||
loop = GLib.MainLoop()
|
||||
self._event_loops = [EventLoopData(loop)]
|
||||
log.debug("GLib event loop is used!")
|
||||
|
||||
@property
|
||||
def active_main_loop(self):
|
||||
"""Return GLib mainloop object."""
|
||||
return self._event_loops[-1].loop
|
||||
|
||||
def register_signal_source(self, signal_source):
|
||||
"""Register source of signal for actual event queue.
|
||||
|
||||
:param signal_source: Source for future signals.
|
||||
:type signal_source: `simpleline.render.ui_screen.UIScreen`
|
||||
"""
|
||||
super().register_signal_source(signal_source)
|
||||
loop_data = self._event_loops[-1]
|
||||
loop_data.sources.add(signal_source)
|
||||
|
||||
def enqueue_signal(self, signal):
|
||||
"""Enqueue new event for processing.
|
||||
|
||||
:param signal: signal which you want to add to the event queue for processing
|
||||
:type signal: instance based on AbstractEvent class
|
||||
"""
|
||||
if self._force_quit:
|
||||
return
|
||||
|
||||
super().enqueue_signal(signal)
|
||||
|
||||
loop_data = self._find_loop_data_for_source(signal.source)
|
||||
self._register_handlers_to_loop(loop_data.loop, signal)
|
||||
|
||||
def _find_loop_data_for_source(self, source):
|
||||
"""Find event loop belonging to this signal source."""
|
||||
for loop_data in reversed(self._event_loops):
|
||||
if source in loop_data.sources:
|
||||
return loop_data
|
||||
|
||||
return self._event_loops[-1]
|
||||
|
||||
def _register_handlers_to_loop(self, event_loop, signal):
|
||||
"""Register handlers to the event loop."""
|
||||
context = event_loop.get_context()
|
||||
handlers = []
|
||||
|
||||
if type(signal) in self._handlers: # pylint: disable=unidiomatic-typecheck
|
||||
handlers = self._handlers[type(signal)]
|
||||
elif isinstance(signal, ExceptionSignal):
|
||||
handler_data = self._create_event_handler(self.kill_app_with_traceback, None)
|
||||
handlers = [handler_data]
|
||||
|
||||
# GLib event source which contains handler callback
|
||||
# Every source can hold only one callback
|
||||
source = GLib.idle_source_new()
|
||||
source.set_priority(signal.priority)
|
||||
data = CallbackArgs(signal, source, handlers)
|
||||
|
||||
source.set_callback(self._run_handlers, data)
|
||||
# attach source to the event loop
|
||||
source.attach(context)
|
||||
|
||||
def _run_handlers(self, data):
|
||||
"""Run handlers attached to this signal and clean source afterwards."""
|
||||
signal = data.signal
|
||||
source = data.source
|
||||
handlers = data.handlers
|
||||
|
||||
if not self._force_quit:
|
||||
try:
|
||||
for handler in handlers:
|
||||
handler.callback(signal, handler.data)
|
||||
except ExitMainLoop:
|
||||
self._quit_all_loops()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.enqueue_signal(ExceptionSignal(self))
|
||||
|
||||
# based on GLib documentation we should clean source
|
||||
# source will be removed from event loop context this way
|
||||
source.destroy()
|
||||
|
||||
self._mark_signal_processed(signal)
|
||||
|
||||
def _quit_all_loops(self):
|
||||
for loop_data in reversed(self._event_loops):
|
||||
loop_data.loop.quit()
|
||||
|
||||
def run(self):
|
||||
"""Starts the event loop."""
|
||||
super().run()
|
||||
if len(self._event_loops) != 1:
|
||||
raise ValueError("Can't run event loop multiple times.")
|
||||
|
||||
self._event_loops[0].loop.run()
|
||||
log.debug("Main loop ended. Running callback if set.")
|
||||
|
||||
if self._quit_callback:
|
||||
cb = self._quit_callback.callback
|
||||
cb(self._quit_callback.args)
|
||||
|
||||
def force_quit(self):
|
||||
"""Force quit all running event loops.
|
||||
|
||||
Kill all loop including inner loops (modal window).
|
||||
None of the Simpleline events will be processed anymore.
|
||||
"""
|
||||
super().force_quit()
|
||||
self._quit_all_loops()
|
||||
|
||||
def execute_new_loop(self, signal):
|
||||
"""Starts the new event loop and pass `signal` in it.
|
||||
|
||||
This is required for processing a modal screens.
|
||||
|
||||
:param signal: signal passed to the new event loop
|
||||
:type signal: `AbstractSignal` based class
|
||||
"""
|
||||
super().execute_new_loop(signal)
|
||||
|
||||
if self._force_quit:
|
||||
return
|
||||
|
||||
new_context = GLib.MainContext()
|
||||
new_loop = GLib.MainLoop(new_context)
|
||||
loop_data = EventLoopData(new_loop)
|
||||
self._event_loops.append(loop_data)
|
||||
|
||||
self.enqueue_signal(signal)
|
||||
new_loop.run()
|
||||
|
||||
def close_loop(self):
|
||||
"""Close active event loop.
|
||||
|
||||
Close an event loop created by the `execute_new_loop()` method.
|
||||
"""
|
||||
super().close_loop()
|
||||
old_loop_data = self._event_loops.pop()
|
||||
old_loop_data.loop.quit()
|
||||
|
||||
def process_signals(self, return_after=None):
|
||||
"""This method processes incoming async messages.
|
||||
|
||||
Process signals en-queued by the `self.enqueue_signal()` method. Call handlers registered
|
||||
to the signals by the `self.register_signal_handler()` method.
|
||||
|
||||
When `return_after` is specified then wait to the point when this signal is processed.
|
||||
NO warranty that this method will return immediately after the signal was processed!
|
||||
|
||||
Without `return_after` parameter this method will return after all queued signals with
|
||||
the highest priority will be processed.
|
||||
|
||||
The method is NOT thread safe!
|
||||
|
||||
:param return_after: Wait on this signal to be processed.
|
||||
:type return_after: Class of the signal.
|
||||
"""
|
||||
super().process_signals(return_after)
|
||||
loop_data = self._event_loops[-1]
|
||||
|
||||
if return_after is not None:
|
||||
ticket_id = self._register_wait_on_signal(return_after)
|
||||
|
||||
while not self._check_if_signal_processed(return_after, ticket_id) and \
|
||||
not self._force_quit:
|
||||
self._iterate_event_loop(loop_data.loop)
|
||||
else:
|
||||
self._iterate_event_loop(loop_data.loop)
|
||||
|
||||
@staticmethod
|
||||
def _iterate_event_loop(event_loop):
|
||||
context = event_loop.get_context()
|
||||
# This is useful for tests
|
||||
wait_on_timeout = False
|
||||
context.iteration(wait_on_timeout)
|
||||
|
||||
|
||||
class EventLoopData():
|
||||
|
||||
def __init__(self, loop):
|
||||
super().__init__()
|
||||
self.loop = loop
|
||||
self.sources = set()
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
# Default event loop for Simpleline 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 threading import Lock
|
||||
|
||||
from simpleline.event_loop import AbstractEventLoop, ExitMainLoop
|
||||
from simpleline.event_loop.event_queue import EventQueue
|
||||
from simpleline.event_loop.signals import ExceptionSignal
|
||||
from simpleline.logging import get_simpleline_logger
|
||||
|
||||
log = get_simpleline_logger()
|
||||
|
||||
__all__ = ["MainLoop"]
|
||||
|
||||
|
||||
class MainLoop(AbstractEventLoop):
|
||||
"""Default main event loop for the Simpleline.
|
||||
|
||||
This event loop can be replaced by your event loop by implementing
|
||||
`simpleline.event_loop.AbstractEventLoop` class.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._active_queue = EventQueue()
|
||||
self._event_queues = [self._active_queue]
|
||||
self._lock = Lock()
|
||||
|
||||
def register_signal_source(self, signal_source):
|
||||
"""Register source of signal for actual event queue.
|
||||
|
||||
:param signal_source: Source for future signals.
|
||||
:type signal_source: `simpleline.render.ui_screen.UIScreen`.
|
||||
"""
|
||||
super().register_signal_source(signal_source)
|
||||
self._active_queue.add_source(signal_source)
|
||||
|
||||
def run(self):
|
||||
"""This methods starts the application.
|
||||
|
||||
Do not use self.mainloop() directly as run() handles all the required exceptions
|
||||
needed to keep nested mainloop working.
|
||||
"""
|
||||
super().run()
|
||||
self._run_loop = True
|
||||
|
||||
try:
|
||||
self._mainloop()
|
||||
except ExitMainLoop:
|
||||
pass
|
||||
|
||||
log.debug("Main loop ended. Running callback if set.")
|
||||
|
||||
if self._quit_callback:
|
||||
cb = self._quit_callback.callback
|
||||
cb(self._quit_callback.args)
|
||||
|
||||
def force_quit(self):
|
||||
"""Force quit all running event loops.
|
||||
|
||||
Kill all loop including inner loops (modal window).
|
||||
None of the Simpleline events will be processed anymore.
|
||||
"""
|
||||
super().force_quit()
|
||||
self._event_queues.clear()
|
||||
self._run_loop = False
|
||||
|
||||
def execute_new_loop(self, signal):
|
||||
"""Starts the new event loop and pass `signal` in it.
|
||||
|
||||
This is required for processing a modal screens.
|
||||
|
||||
:param signal: Signal passed to the new event loop.
|
||||
:type signal: The `AbstractSignal` based class.
|
||||
"""
|
||||
super().execute_new_loop(signal)
|
||||
|
||||
if self._force_quit:
|
||||
return
|
||||
|
||||
self._active_queue = EventQueue()
|
||||
|
||||
# TODO: Remove when python3-astroid 1.5.3 will be in Fedora
|
||||
# pylint: disable=not-context-manager
|
||||
with self._lock:
|
||||
self._event_queues.append(self._active_queue)
|
||||
|
||||
self.enqueue_signal(signal)
|
||||
self._mainloop()
|
||||
log.debug("Inner loop is closed")
|
||||
|
||||
def close_loop(self):
|
||||
"""Close active event loop.
|
||||
|
||||
Close an event loop created by the `execute_new_loop()` method.
|
||||
"""
|
||||
super().close_loop()
|
||||
self.process_signals()
|
||||
|
||||
# TODO: Remove when python3-astroid 1.5.3 will be in Fedora
|
||||
# pylint: disable=not-context-manager
|
||||
with self._lock:
|
||||
self._event_queues.pop()
|
||||
try:
|
||||
self._active_queue = self._event_queues[-1]
|
||||
except IndexError:
|
||||
log.error("No more event queues to work with!")
|
||||
raise ExitMainLoop() # pylint: disable=raise-missing-from
|
||||
|
||||
self._run_loop = False
|
||||
|
||||
def enqueue_signal(self, signal):
|
||||
"""Enqueue new event for processing.
|
||||
|
||||
Enqueue signal to the most inner queue (nearest to the active queue) where
|
||||
the `signal.source` belongs.
|
||||
If it belongs nowhere enqueue it to the active one.
|
||||
|
||||
This method is thread safe.
|
||||
|
||||
:param signal: Event which you want to add to the event queue for processing.
|
||||
:type signal: Instance based on AbstractEvent class.
|
||||
"""
|
||||
if self._force_quit:
|
||||
return
|
||||
|
||||
super().enqueue_signal(signal)
|
||||
# TODO: Remove when python3-astroid 1.5.3 will be in Fedora
|
||||
# pylint: disable=not-context-manager
|
||||
with self._lock:
|
||||
for queue in reversed(self._event_queues):
|
||||
if queue.enqueue_if_source_belongs(signal, signal.source):
|
||||
return
|
||||
|
||||
self._active_queue.enqueue(signal)
|
||||
|
||||
def _mainloop(self):
|
||||
"""Single mainloop. Do not use directly, start the application using run()."""
|
||||
# run infinite loop
|
||||
# this will always wait on input processing or similar so it should not busy waiting
|
||||
while self._run_loop:
|
||||
self._process_signals_loop()
|
||||
|
||||
if not self._force_quit:
|
||||
# set back to True to leave outer loop working
|
||||
self._run_loop = True
|
||||
|
||||
def process_signals(self, return_after=None):
|
||||
"""This method processes incoming async messages.
|
||||
|
||||
Process signals en-queued by the `self.enqueue_signal()` method. Call handlers
|
||||
registered to the signals by the `self.register_signal_handler()` method.
|
||||
|
||||
When `return_after` is specified then wait to the point when this signal is processed.
|
||||
NO warranty that this method will return immediately after the signal was processed!
|
||||
|
||||
Without `return_after` parameter this method will return after all queued signals
|
||||
with the highest priority will be processed.
|
||||
|
||||
The method is NOT thread safe!
|
||||
|
||||
:param return_after: Wait on this signal to be processed.
|
||||
:type return_after: Class of the signal.
|
||||
"""
|
||||
super().process_signals(return_after)
|
||||
if return_after is not None:
|
||||
self._process_signals_with_return(return_after)
|
||||
else:
|
||||
self._process_signals_iteration()
|
||||
|
||||
def _process_signals_with_return(self, return_after):
|
||||
"""Process signals until the return_after signal was processed.
|
||||
|
||||
Or the loop quited.
|
||||
"""
|
||||
# get unique ID when waiting for the signal
|
||||
unique_id = self._register_wait_on_signal(return_after)
|
||||
|
||||
while self._run_loop:
|
||||
signal = self._active_queue.get()
|
||||
|
||||
# do the signal processing (call handlers)
|
||||
self._process_signal(signal)
|
||||
|
||||
# was our signal processed if yes, return this method
|
||||
if self._check_if_signal_processed(return_after, unique_id):
|
||||
return
|
||||
|
||||
def _process_signals_iteration(self):
|
||||
"""Process queued signal and then return."""
|
||||
priority = None
|
||||
|
||||
while not self._active_queue.empty() and self._run_loop:
|
||||
if priority is None:
|
||||
# take first signal to find out the highest priority in queue
|
||||
signal = self._active_queue.get()
|
||||
priority = signal.priority
|
||||
else:
|
||||
# get signal with this priority only
|
||||
signal = self._active_queue.get_top_event_if_priority(priority)
|
||||
|
||||
# Signal with this priority is not available anymore
|
||||
if signal is None:
|
||||
return
|
||||
|
||||
self._process_signal(signal)
|
||||
|
||||
def _process_signals_loop(self):
|
||||
"""Process signal until the event loop quited."""
|
||||
while self._run_loop:
|
||||
signal = self._active_queue.get()
|
||||
self._process_signal(signal)
|
||||
|
||||
def _process_signal(self, signal):
|
||||
log.debug("Processing signal %s", signal)
|
||||
|
||||
self._mark_signal_processed(signal)
|
||||
|
||||
if type(signal) in self._handlers: # pylint: disable=unidiomatic-typecheck
|
||||
for handler_data in self._handlers[type(signal)]:
|
||||
try:
|
||||
handler_data.callback(signal, handler_data.data)
|
||||
except ExitMainLoop: # pylint: disable=try-except-raise
|
||||
raise
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.enqueue_signal(ExceptionSignal(self))
|
||||
elif isinstance(signal, ExceptionSignal):
|
||||
self.kill_app_with_traceback(signal)
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
# Set of default signals used inside of 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 sys import exc_info
|
||||
from simpleline.event_loop import AbstractSignal
|
||||
|
||||
__all__ = ["ExceptionSignal", "InputReadySignal", "RenderScreenSignal", "CloseScreenSignal",
|
||||
"InputReceivedSignal"]
|
||||
|
||||
|
||||
class ExceptionSignal(AbstractSignal):
|
||||
"""Emit this signal when exception is raised.
|
||||
|
||||
This class must be created inside of exception handler or `exception_info` must be specified
|
||||
in creation process.
|
||||
|
||||
If you register handler for this exception then the Simpleline's exception handling
|
||||
is disabled!
|
||||
"""
|
||||
|
||||
def __init__(self, source, exception_info=None):
|
||||
"""Create exception signal with higher priority (-20) than other signals.
|
||||
|
||||
:param source: source of this signal
|
||||
:type source: class which emits this signal
|
||||
|
||||
:param exception_info: if specified raise your exception, otherwise create exception here;
|
||||
to create exception here it needs to be created inside of exception
|
||||
handler
|
||||
:type exception_info: output of `sys.exc_info()` method
|
||||
"""
|
||||
super().__init__(source, priority=-20)
|
||||
if exception_info:
|
||||
self.exception_info = exception_info
|
||||
else:
|
||||
self.exception_info = exc_info()
|
||||
|
||||
|
||||
class InputReadySignal(AbstractSignal):
|
||||
"""Input from user is ready for processing."""
|
||||
def __init__(self, source, input_handler_source, data, priority=0, success=True):
|
||||
"""Store user input inside of this signal
|
||||
|
||||
Read the data from user input in `data` attribute.
|
||||
|
||||
The only way how a user should ask for input is to use InputHandler and inherited classes.
|
||||
The input_handler_source param must be set but this signal instance can be attached to
|
||||
another source object which is registered to a specific event loop.
|
||||
|
||||
If no requester (object who uses InputHandler) is specified then source and
|
||||
input_handler_source will both point to InputHandler instance.
|
||||
|
||||
:param source: Source of this signal.
|
||||
:type source: Any object.
|
||||
|
||||
:param input_handler_source: InputHandler who is asking for input.
|
||||
:type input_handler_source: The `simpleline.input.input_handler.InputHandler` based
|
||||
instance.
|
||||
|
||||
:param data: User input data.
|
||||
:type data: str
|
||||
|
||||
:param priority: Priority of this event.
|
||||
:type priority: Int greater than 0.
|
||||
|
||||
:param success: Was the input successful? True on successful input False otherwise.
|
||||
:type success: bool
|
||||
"""
|
||||
super().__init__(source, priority=priority)
|
||||
self.input_handler_source = input_handler_source
|
||||
self.data = data
|
||||
self.success = success
|
||||
|
||||
|
||||
class InputReceivedSignal(AbstractSignal):
|
||||
"""Raw input received.
|
||||
|
||||
This signal will be further processed and InputReadySignal should be enqueued soon.
|
||||
Most probably you are looking for InputReadySignal instead.
|
||||
"""
|
||||
def __init__(self, source, data, priority=0):
|
||||
super().__init__(source, priority=priority)
|
||||
self.data = data
|
||||
|
||||
|
||||
class RenderScreenSignal(AbstractSignal):
|
||||
"""Render UIScreen to terminal."""
|
||||
|
||||
|
||||
class CloseScreenSignal(AbstractSignal):
|
||||
"""Close current screen."""
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# Ticket machine synchronization class.
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
|
||||
|
||||
class TicketMachine():
|
||||
"""Hold signals processed by the event loop if someone wait on them.
|
||||
|
||||
This is useful when recursive process events will skip required signal.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._lines = {}
|
||||
self._counter = 0
|
||||
|
||||
def take_ticket(self, line_id):
|
||||
"""Take ticket (id) and go line (processing events).
|
||||
|
||||
Use `check_ticket` if you are ready to go.
|
||||
|
||||
:param line_id: Line where you are waiting.
|
||||
:type line_id: Anything.
|
||||
"""
|
||||
obj_id = self._counter
|
||||
if line_id not in self._lines:
|
||||
self._lines[line_id] = {obj_id: False}
|
||||
else:
|
||||
self._lines[line_id][obj_id] = False
|
||||
|
||||
self._counter += 1
|
||||
return obj_id
|
||||
|
||||
def check_ticket(self, line, unique_id):
|
||||
"""Check if you are ready to go.
|
||||
|
||||
If True the unique_id is not valid anymore.
|
||||
|
||||
:param unique_id: Your id used to identify you in the line.
|
||||
:type unique_id: int
|
||||
|
||||
:param line: Line where you are waiting.
|
||||
:type line: Anything.
|
||||
|
||||
:return: True if the ticket was already marked, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
if self._lines[line][unique_id]:
|
||||
return self._lines[line].pop(unique_id)
|
||||
|
||||
return False
|
||||
|
||||
def mark_line_to_go(self, line):
|
||||
"""All in the `line` are ready to go.
|
||||
|
||||
Mark all tickets in the line as True.
|
||||
|
||||
:param line: Line which should processed.
|
||||
:type line: Anything.
|
||||
"""
|
||||
if line in self._lines:
|
||||
our_line = self._lines[line]
|
||||
for key in our_line:
|
||||
our_line[key] = True
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
# Global configuration for the whole 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/>.
|
||||
#
|
||||
|
||||
from getpass import getpass
|
||||
|
||||
__all__ = ["GlobalConfiguration"]
|
||||
|
||||
DEFAULT_WIDTH = 80
|
||||
DEFAULT_PASSWORD_FUNC = getpass
|
||||
|
||||
|
||||
class GlobalConfiguration():
|
||||
"""Class for global configuration of application defaults.
|
||||
|
||||
All stored data are persistent between App.initialize() calls and can be used before this call.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._width = DEFAULT_WIDTH
|
||||
self._getpass = DEFAULT_PASSWORD_FUNC
|
||||
self._run_with_empty_stack = False
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""Get width of the application.
|
||||
|
||||
:returns: int
|
||||
"""
|
||||
return self._width
|
||||
|
||||
@width.setter
|
||||
def width(self, width):
|
||||
"""Set width of the application.
|
||||
|
||||
:param width: Number of characters which can be printed to one line.
|
||||
:type width: int
|
||||
"""
|
||||
self._width = width
|
||||
|
||||
def clear_width(self):
|
||||
"""Clear user defined width and set the default.
|
||||
|
||||
Default: 80 characters
|
||||
"""
|
||||
self._width = DEFAULT_WIDTH
|
||||
|
||||
@property
|
||||
def password_function(self):
|
||||
"""Get function to get user passwords from a console.
|
||||
|
||||
:returns: Function with one argument which is text representation of prompt.
|
||||
"""
|
||||
return self._getpass
|
||||
|
||||
@password_function.setter
|
||||
def password_function(self, password_func):
|
||||
"""Set function to get user passwords from a console.
|
||||
|
||||
:param password_func: Function to get password from a command line.
|
||||
:type password_func: Function with one argument which is text representation of prompt.
|
||||
"""
|
||||
self._getpass = password_func
|
||||
|
||||
def clear_password_function(self):
|
||||
"""Clear user defined password function and set the default.
|
||||
|
||||
Default: getpass.getpass function
|
||||
"""
|
||||
self._getpass = getpass
|
||||
|
||||
@property
|
||||
def should_run_with_empty_stack(self):
|
||||
"""Should test on empty screen stack when starting event loop.
|
||||
|
||||
:returns: If False the App.run() call will end with an exception (default), True otherwise.
|
||||
"""
|
||||
return self._run_with_empty_stack
|
||||
|
||||
@should_run_with_empty_stack.setter
|
||||
def should_run_with_empty_stack(self, value):
|
||||
"""Set if the App.run() call should end with an exception when screen stack is empty.
|
||||
|
||||
This can be valuable when you want to schedule a screen later by an other thread.
|
||||
|
||||
:param value: If False the App.run() call will end with an exception, if True it will
|
||||
run with nothing displayed.
|
||||
"""
|
||||
self._run_with_empty_stack = value
|
||||
|
||||
def clear_should_run_with_empty_stack(self):
|
||||
"""Clear user defined test to run with an empty screen stack.
|
||||
|
||||
Default: False
|
||||
"""
|
||||
self._run_with_empty_stack = False
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# 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 simpleline.input.input_handler import InputHandler, PasswordInputHandler
|
||||
|
||||
__all__ = ["InputHandler", "PasswordInputHandler"]
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
# Handle user 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/>.
|
||||
#
|
||||
|
||||
import sys
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.event_loop.signals import InputReadySignal
|
||||
from simpleline.render.widgets import TextWidget
|
||||
from simpleline.input.input_threading import InputThreadManager, InputRequest
|
||||
|
||||
__all__ = ["InputHandler", "PasswordInputHandler"]
|
||||
|
||||
|
||||
class InputHandler():
|
||||
|
||||
def __init__(self, callback=None, source=None):
|
||||
"""Class to handle input from the terminal.
|
||||
|
||||
This class is designed to be instantiated on place where it should be used.
|
||||
The main method is `get_input()` which is non-blocking asynchronous call. It can be used
|
||||
as synchronous call be calling the `wait_on_input` method.
|
||||
|
||||
To get result from this class use the `value` property.
|
||||
|
||||
:param callback: You can specify callback which will be called when user give input.
|
||||
:type callback: Callback function with one argument which will be user input.
|
||||
|
||||
:param source: Source of this input. It will be helpful in case of debugging an issue.
|
||||
:type source: Class which will process an input from this InputHandler.
|
||||
"""
|
||||
super().__init__()
|
||||
self._input = None
|
||||
self._input_callback = callback
|
||||
self._input_received = False
|
||||
self._input_successful = False
|
||||
self._skip_concurrency_check = False
|
||||
self._source = source
|
||||
|
||||
App.get_event_loop().register_signal_handler(InputReadySignal,
|
||||
self._input_received_handler)
|
||||
|
||||
def _input_received_handler(self, signal, args):
|
||||
if signal.input_handler_source != self:
|
||||
return
|
||||
|
||||
self._input_received = True
|
||||
self._input_successful = signal.success
|
||||
|
||||
if not self._input_successful:
|
||||
return
|
||||
|
||||
self._input = signal.data
|
||||
|
||||
# call async callback
|
||||
if self._input_callback is not None:
|
||||
cb = self._input_callback
|
||||
self._input_callback = None
|
||||
|
||||
cb(self._input)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Return user input.
|
||||
|
||||
:returns: String or None if no is input received.
|
||||
"""
|
||||
return self._input
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Get source of this input.
|
||||
|
||||
:returns: Anything probably UIScreen.
|
||||
"""
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def skip_concurrency_check(self):
|
||||
"""Is this InputHandler skipping concurrency check?
|
||||
|
||||
:returns bool
|
||||
"""
|
||||
return self._skip_concurrency_check
|
||||
|
||||
@skip_concurrency_check.setter
|
||||
def skip_concurrency_check(self, value):
|
||||
"""Set if this InputHandler should skip concurrency check.
|
||||
|
||||
Note if you skip this check, you can have unexpected behavior. Use with caution.
|
||||
|
||||
:param value: True to skip the check, False if not.
|
||||
"""
|
||||
self._skip_concurrency_check = value
|
||||
|
||||
def set_callback(self, callback):
|
||||
"""Set a callback to get user input asynchronously.
|
||||
|
||||
:param callback: Callback called when user write their input.
|
||||
:type callback: Method with 1 argument which is user input: def cb(user_input)
|
||||
"""
|
||||
self._input_callback = callback
|
||||
|
||||
def input_received(self):
|
||||
"""Was user input already received?
|
||||
|
||||
:returns: True if yes, False otherwise.
|
||||
"""
|
||||
return self._input_received
|
||||
|
||||
def wait_on_input(self):
|
||||
"""Blocks execution till the user input is received.
|
||||
|
||||
Events will works as expected during this blocking.
|
||||
|
||||
Please check the `input_successful` method to test the input.
|
||||
"""
|
||||
# we already received input from user
|
||||
if self._input_received:
|
||||
return
|
||||
|
||||
while not self._input_received:
|
||||
App.get_event_loop().process_signals(InputReadySignal)
|
||||
|
||||
def input_successful(self):
|
||||
"""Was input successful?
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
return self._input_successful
|
||||
|
||||
def get_input(self, prompt):
|
||||
"""Use prompt to ask for user input and wait (non-blocking) on user input.
|
||||
|
||||
This is an asynchronous call. If you want to wait for user input then use
|
||||
the `wait_on_input` method. If you want to get results asynchronously then register
|
||||
callback in constructor or by the `set_callback` method.
|
||||
|
||||
Check if user input was already received can be done by the `input_received` method call.
|
||||
|
||||
:param prompt: Ask user what you want to get.
|
||||
:type prompt: String or Prompt instance.
|
||||
|
||||
:returns: User input.
|
||||
:rtype: str
|
||||
"""
|
||||
self._clear_input()
|
||||
self._invoke_input_thread(prompt)
|
||||
|
||||
def _invoke_input_thread(self, prompt):
|
||||
thread_object = self.create_thread_object(prompt)
|
||||
InputThreadManager.get_instance().start_input_thread(thread_object,
|
||||
not self._skip_concurrency_check)
|
||||
|
||||
def create_thread_object(self, prompt):
|
||||
"""Create thread object containing all the information how to get user input.
|
||||
|
||||
:returns: Instance of class inherited from `simpleline.input.InputThread`.
|
||||
"""
|
||||
return InputHandlerRequest(App.get_configuration().width, prompt, self)
|
||||
|
||||
def _clear_input(self):
|
||||
self._input_received = False
|
||||
self._input = None
|
||||
|
||||
|
||||
class InputHandlerRequest(InputRequest):
|
||||
"""This is thread object to get input from user without blocking main thread."""
|
||||
|
||||
def __init__(self, width, prompt, input_handler):
|
||||
"""Create request object to get input in InputThreadManager.
|
||||
|
||||
:param width: Width of the screen prompt.
|
||||
:type width: int
|
||||
|
||||
:param prompt: Input prompt.
|
||||
:type prompt: Instance of `simpleline.render.prompt.Prompt` class.
|
||||
|
||||
:param input_handler: InputHandler instance which created this object.
|
||||
:type input_handler: InputHandler based instance.
|
||||
"""
|
||||
super().__init__(input_handler, input_handler.source)
|
||||
self._width = width
|
||||
self._prompt = prompt
|
||||
|
||||
def get_input(self):
|
||||
"""This method is responsible for interruptable user input.
|
||||
|
||||
It is expected to be used in a thread started on demand
|
||||
and returns the input via the communication Queue.
|
||||
"""
|
||||
# lock acquired, we can run input
|
||||
try:
|
||||
data = self._ask_input()
|
||||
except EOFError:
|
||||
data = ""
|
||||
|
||||
return data
|
||||
|
||||
def text_prompt(self):
|
||||
widget = TextWidget(str(self._prompt))
|
||||
widget.render(self._width)
|
||||
lines = widget.get_lines()
|
||||
return "\n".join(lines) + " "
|
||||
|
||||
def _ask_input(self):
|
||||
text_prompt = self.text_prompt()
|
||||
sys.stdout.write(text_prompt)
|
||||
sys.stdout.flush()
|
||||
|
||||
return self._get_input()
|
||||
|
||||
@staticmethod
|
||||
def _get_input():
|
||||
return input()
|
||||
|
||||
|
||||
class PasswordInputHandler(InputHandler):
|
||||
|
||||
def __init__(self, callback=None, source=None):
|
||||
"""Class to handle hidden password input from the terminal.
|
||||
|
||||
This class is designed to be instantiated on place where it should be used.
|
||||
The main method is `get_input()` which is non-blocking asynchronous call. It can be used
|
||||
as synchronous call be calling the `wait_on_input` method.
|
||||
|
||||
To get result from this class use the `value` property.
|
||||
|
||||
:param callback: You can specify callback which will be called when user give input.
|
||||
:type callback: Callback function with one argument which will be user input.
|
||||
|
||||
:param source: Source of this input. It will be helpful in case of debugging an issue.
|
||||
:type source: Class which will process an input from this InputHandler.
|
||||
"""
|
||||
super().__init__(callback=callback, source=source)
|
||||
self._getpass_func = App.get_configuration().password_function
|
||||
|
||||
def set_pass_func(self, getpass_func):
|
||||
"""Set a function for getting passwords."""
|
||||
if not getpass_func:
|
||||
return
|
||||
|
||||
self._getpass_func = getpass_func
|
||||
|
||||
def create_thread_object(self, prompt):
|
||||
"""Return PasswordInputThread for getting user password."""
|
||||
return PasswordInputHandlerRequest(App.get_configuration().width, prompt, self,
|
||||
self._getpass_func)
|
||||
|
||||
|
||||
class PasswordInputHandlerRequest(InputHandlerRequest):
|
||||
"""Similar as InputHandlerRequest but don't echo user keys."""
|
||||
|
||||
def __init__(self, width, prompt, input_handler, getpass_func):
|
||||
"""Create request object to get password input in InputThreadManager.
|
||||
|
||||
:param width: Width of the screen prompt.
|
||||
:type width: int
|
||||
|
||||
:param prompt: Input prompt.
|
||||
:type prompt: Instance of `simpleline.render.prompt.Prompt` class.
|
||||
|
||||
:param input_handler: InputHandler instance which created this object.
|
||||
:type input_handler: InputHandler based instance.
|
||||
|
||||
:param getpass_func: Function to get user password.
|
||||
:type getpass_func: Function which gets prompt as only parameter and returns user input
|
||||
string.
|
||||
"""
|
||||
super().__init__(width, prompt, input_handler)
|
||||
self._getpass_func = getpass_func
|
||||
|
||||
def _ask_input(self):
|
||||
text_prompt = self.text_prompt()
|
||||
|
||||
return self._getpass_func(text_prompt)
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
#
|
||||
# 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 threading
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.logging import get_simpleline_logger
|
||||
from simpleline.event_loop.signals import InputReceivedSignal, InputReadySignal
|
||||
|
||||
log = get_simpleline_logger()
|
||||
|
||||
|
||||
INPUT_THREAD_NAME = "SimplelineInputThread"
|
||||
|
||||
|
||||
class InputThreadManager():
|
||||
"""Manager object for input threads.
|
||||
|
||||
This manager helps with concurrent user input (still you really shouldn't do that).
|
||||
"""
|
||||
|
||||
__instance = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._input_stack = []
|
||||
self._processing_input = False
|
||||
|
||||
@classmethod
|
||||
def create_new_instance(cls):
|
||||
instance = InputThreadManager()
|
||||
cls.__instance = instance
|
||||
|
||||
instance._post_init_configuration() # pylint: disable=protected-access
|
||||
|
||||
def _post_init_configuration(self):
|
||||
# pylint: disable=protected-access
|
||||
App.get_event_loop().register_signal_handler(InputReceivedSignal,
|
||||
self.__instance._input_received_handler)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if not cls.__instance:
|
||||
cls.create_new_instance()
|
||||
|
||||
return cls.__instance
|
||||
|
||||
def _input_received_handler(self, signal, args):
|
||||
thread_object = self._input_stack.pop()
|
||||
thread_object.emit_input_ready_signal(signal.data)
|
||||
|
||||
if thread_object.thread:
|
||||
thread_object.thread.join()
|
||||
|
||||
# wait until used object ends
|
||||
for t in self._input_stack:
|
||||
t.emit_failed_input_ready_signal()
|
||||
if t.thread:
|
||||
t.thread.join()
|
||||
|
||||
# remove all other items waiting for input
|
||||
self._input_stack.clear()
|
||||
self._processing_input = False
|
||||
|
||||
def start_input_thread(self, input_thread_object, concurrent_check=True):
|
||||
"""Start input thread to get user input.
|
||||
|
||||
:param input_thread_object: Input thread object based on InputThread class.
|
||||
:param concurrent_check: Should the concurrent thread check be fatal? (default True).
|
||||
"""
|
||||
self._input_stack.append(input_thread_object)
|
||||
self._check_input_thread_running(concurrent_check)
|
||||
self._start_user_input_async()
|
||||
|
||||
def _check_input_thread_running(self, raise_concurrent_check):
|
||||
if len(self._input_stack) != 1:
|
||||
if not raise_concurrent_check:
|
||||
log.warning("Asking for multiple inputs with concurrent check bypassed, "
|
||||
"last who asked wins! Others are dropped.")
|
||||
else:
|
||||
msg = ""
|
||||
for t in self._input_stack:
|
||||
requester_source = t.requester_source or "Unknown"
|
||||
msg += "Input handler: {} Input requester: {}\n".format(t.source,
|
||||
requester_source)
|
||||
|
||||
msg.rstrip()
|
||||
|
||||
raise KeyError("Can't run multiple input threads at the same time!\n"
|
||||
"Asking for input:\n"
|
||||
"{}".format(msg))
|
||||
|
||||
def _start_user_input_async(self):
|
||||
thread_object = self._input_stack[-1]
|
||||
|
||||
if self._processing_input:
|
||||
self._print_new_prompt(thread_object)
|
||||
return
|
||||
|
||||
thread_object.initialize_thread()
|
||||
self._processing_input = True
|
||||
thread_object.start_thread()
|
||||
|
||||
@staticmethod
|
||||
def _print_new_prompt(thread_object):
|
||||
prompt = thread_object.text_prompt()
|
||||
|
||||
# print new prompt
|
||||
print(prompt, end="")
|
||||
|
||||
|
||||
class InputRequest(metaclass=ABCMeta):
|
||||
"""Base input request class.
|
||||
|
||||
This should be overloaded for every InputHandler class. Purpose of this class is to print
|
||||
prompt and get input from user.
|
||||
|
||||
The `run_input` method is the entry point for this class. Output from this method must be
|
||||
a user input.
|
||||
The `text_prompt` method is used to get textual representation of a prompt. This will be used
|
||||
on concurrent input to replace existing prompt to get new input.
|
||||
|
||||
WARNING:
|
||||
The `run_input` method will run in a separate thread!
|
||||
"""
|
||||
|
||||
def __init__(self, source, requester_source=None):
|
||||
super().__init__()
|
||||
self._source = source
|
||||
self._requester_source = requester_source
|
||||
self.thread = None
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Get direct source of this input request.
|
||||
|
||||
:returns: InputHandler instance.
|
||||
"""
|
||||
return self._source
|
||||
|
||||
@property
|
||||
def requester_source(self):
|
||||
"""Get requester -- source of this input.
|
||||
|
||||
:returns: Anything probably UIScreen based instance.
|
||||
"""
|
||||
return self._requester_source
|
||||
|
||||
def emit_input_ready_signal(self, input_data):
|
||||
"""Emit the InputReadySignal signal with collected input data.
|
||||
|
||||
:param input_data: Input data received.
|
||||
:type input_data: str
|
||||
"""
|
||||
handler_source = self.source
|
||||
signal_source = self._get_request_source()
|
||||
|
||||
new_signal = InputReadySignal(source=signal_source, input_handler_source=handler_source,
|
||||
data=input_data, success=True)
|
||||
App.get_event_loop().enqueue_signal(new_signal)
|
||||
|
||||
def emit_failed_input_ready_signal(self):
|
||||
"""Emit the InputReadySignal with failed state."""
|
||||
handler_source = self.source
|
||||
signal_source = self._get_request_source()
|
||||
|
||||
new_signal = InputReadySignal(source=signal_source, input_handler_source=handler_source,
|
||||
data="", success=False)
|
||||
App.get_event_loop().enqueue_signal(new_signal)
|
||||
|
||||
def _get_request_source(self):
|
||||
"""Get user input request source.
|
||||
|
||||
That means object who is using InputHandler.
|
||||
If this object is not specified then return InputHandler as a source.
|
||||
"""
|
||||
return self.requester_source or self.source
|
||||
|
||||
def initialize_thread(self):
|
||||
"""Initialize thread for this input request.
|
||||
|
||||
Do not call this directly! Will be called by InputThreadManager.
|
||||
"""
|
||||
self.thread = threading.Thread(name=INPUT_THREAD_NAME, target=self.run)
|
||||
self.thread.daemon = True
|
||||
|
||||
def start_thread(self):
|
||||
"""Start input thread.
|
||||
|
||||
Do not call this directly! Will be called by InputThreadManager.
|
||||
"""
|
||||
self.thread.start()
|
||||
|
||||
def run(self):
|
||||
"""Run the `run_input` method and propagate input outside.
|
||||
|
||||
Do not call this method directly. It will be called by InputThreadManager.
|
||||
"""
|
||||
data = self.get_input()
|
||||
|
||||
App.get_event_loop().enqueue_signal(InputReceivedSignal(self, data))
|
||||
|
||||
@abstractmethod
|
||||
def text_prompt(self):
|
||||
"""Get text representation of the user prompt.
|
||||
|
||||
This will be used to get high priority input.
|
||||
|
||||
:returns: String representation of the prompt or None if no prompt is present.
|
||||
"""
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
def get_input(self):
|
||||
"""Print prompt and get an input from user.
|
||||
|
||||
..NOTE: Overload this method in your class.
|
||||
|
||||
Return this input from a function.
|
||||
"""
|
||||
return ""
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
# Logging functions and methods used by Simpleline.
|
||||
#
|
||||
# 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 logging
|
||||
|
||||
|
||||
SIMPLELINE_LOGGER = "simpleline"
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""Set proper logging for a library"""
|
||||
log = get_simpleline_logger()
|
||||
null_hd = logging.NullHandler()
|
||||
log.addHandler(null_hd)
|
||||
|
||||
|
||||
def get_simpleline_logger():
|
||||
"""Return logging instance that can be used in the application."""
|
||||
return logging.getLogger(SIMPLELINE_LOGGER)
|
||||
+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
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# iutil.py - generic install utility functions
|
||||
#
|
||||
# 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
|
||||
import string # pylint: disable=deprecated-module
|
||||
import unicodedata
|
||||
|
||||
|
||||
def ensure_str(str_or_bytes, keep_none=True):
|
||||
"""
|
||||
Returns a str instance for given string or ``None`` if requested to keep it.
|
||||
|
||||
:param str_or_bytes: string to be kept or converted to str type
|
||||
:type str_or_bytes: str or bytes
|
||||
:param bool keep_none: whether to keep None as it is or raise ValueError if
|
||||
``None`` is passed
|
||||
:raises ValueError: if applied on an object not being of type bytes nor str
|
||||
(nor NoneType if ``keep_none`` is ``False``)
|
||||
"""
|
||||
if keep_none and str_or_bytes is None:
|
||||
return None
|
||||
if isinstance(str_or_bytes, str):
|
||||
return str_or_bytes
|
||||
if isinstance(str_or_bytes, bytes):
|
||||
return str_or_bytes.decode(sys.getdefaultencoding())
|
||||
|
||||
raise ValueError(
|
||||
"str_or_bytes must be of type 'str' or 'bytes', not '%s'" % type(str_or_bytes))
|
||||
|
||||
|
||||
# Define translations between ASCII uppercase and lowercase for
|
||||
# locale-independent string conversions. The tables are 256-byte string used
|
||||
# with str.translate. If str.translate is used with a unicode string,
|
||||
# even if the string contains only 7-bit characters, str.translate will
|
||||
# raise a UnicodeDecodeError.
|
||||
_ASCIIlower_table = str.maketrans(string.ascii_uppercase, string.ascii_lowercase)
|
||||
_ASCIIupper_table = str.maketrans(string.ascii_lowercase, string.ascii_uppercase)
|
||||
|
||||
|
||||
def _toASCII(s):
|
||||
"""Convert a unicode string to ASCII"""
|
||||
if isinstance(s, str):
|
||||
# Decompose the string using the NFK decomposition, which in addition
|
||||
# to the canonical decomposition replaces characters based on
|
||||
# compatibility equivalence (e.g., ROMAN NUMERAL ONE has its own code
|
||||
# point but it's really just a capital I), so that we can keep as much
|
||||
# of the ASCII part of the string as possible.
|
||||
s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode("ascii")
|
||||
elif not isinstance(s, bytes):
|
||||
s = ''
|
||||
return s
|
||||
|
||||
|
||||
def lowerASCII(s):
|
||||
"""Convert a string to lowercase using only ASCII character definitions.
|
||||
|
||||
The returned string will contain only ASCII characters. This function is
|
||||
locale-independent.
|
||||
"""
|
||||
# XXX: Python 3 has str.maketrans() and bytes.maketrans() so we should
|
||||
# ideally use one or the other depending on the type of 's'. But it turns
|
||||
# out we expect this function to always return string even if given bytes.
|
||||
s = ensure_str(s)
|
||||
return str.translate(_toASCII(s), _ASCIIlower_table)
|
||||
|
||||
|
||||
def upperASCII(s):
|
||||
"""Convert a string to uppercase using only ASCII character definitions.
|
||||
|
||||
The returned string will contain only ASCII characters. This function is
|
||||
locale-independent.
|
||||
"""
|
||||
# XXX: Python 3 has str.maketrans() and bytes.maketrans() so we should
|
||||
# ideally use one or the other depending on the type of 's'. But it turns
|
||||
# out we expect this function to always return string even if given bytes.
|
||||
s = ensure_str(s)
|
||||
return str.translate(_toASCII(s), _ASCIIupper_table)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
# Translation functions we use all over the place
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
__all__ = ["_", "N_", "P_", "C_", "CN_", "CP_"]
|
||||
|
||||
import gettext
|
||||
|
||||
N_ = lambda x: x
|
||||
_ = lambda x: gettext.translation("python-simpleline", fallback=True).gettext(x) if x != "" else ""
|
||||
P_ = lambda x, y, z: gettext.translation("python-simpleline", fallback=True).ngettext(x, y, z)
|
||||
|
||||
# This is equivalent to "pgettext" in GNU gettext. The pgettext functions
|
||||
# are not exported by Python, but all they really do is a stick a EOT
|
||||
# character between msgctxt and msgid and check that msgctxt isn't part
|
||||
# of the return value.
|
||||
|
||||
|
||||
def C_(msgctxt, msgid):
|
||||
ctxid = "%s\x04%s" % (msgctxt, msgid)
|
||||
translation = _(ctxid)
|
||||
|
||||
# If there is no translation for msgctxt<EOT>msgid, return only msgid
|
||||
if translation == ctxid:
|
||||
return msgid
|
||||
|
||||
return translation
|
||||
|
||||
# Mark as translatable with context
|
||||
CN_ = lambda c, x: x
|
||||
|
||||
# npgettext; i.e., gettext with plural form and context
|
||||
|
||||
|
||||
def CP_(msgctxt, msgid, msgid_plural, n):
|
||||
ctxid = "%s\x04%s" % (msgctxt, msgid)
|
||||
translation = P_(ctxid, msgid_plural, n)
|
||||
|
||||
# If the returned value is msgctxt<EOT>msgid, ngettext was trying to
|
||||
# fallback to msgid. We don't add msgctxt to msgid_plural, so any other
|
||||
# return value is correct.
|
||||
if translation == ctxid:
|
||||
return msgid
|
||||
|
||||
return translation
|
||||
Reference in New Issue
Block a user