feat(Anaconda): Local Repo
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/home/nocode/Files/FWS-ISO/pkgbuilds/python-simpleline/python-simpleline-1.8.tar.gz
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
name: validate
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
rawhide:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: registry.fedoraproject.org/fedora:rawhide
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: install test dependencies
|
||||
run: dnf install -y python3-pylint python3-gobject-base make
|
||||
|
||||
- name: run tests
|
||||
run: make ci
|
||||
@@ -0,0 +1,23 @@
|
||||
*.pyc
|
||||
.*.swp
|
||||
|
||||
.coverage
|
||||
tests/coverage-report.log
|
||||
|
||||
ChangeLog
|
||||
|
||||
po/*.gmo
|
||||
po/*.po
|
||||
po/python-simpleline.pot
|
||||
po/POTFILES
|
||||
|
||||
tests/pylint/.pylint.d/
|
||||
pylint-log
|
||||
|
||||
build/
|
||||
dist/
|
||||
simpleline.egg-info/
|
||||
|
||||
.directory
|
||||
|
||||
doc/_build
|
||||
@@ -0,0 +1,46 @@
|
||||
specfile_path: python-simpleline.spec
|
||||
upstream_package_name: simpleline
|
||||
upstream_tag_template: simpleline-{version}
|
||||
actions:
|
||||
create-archive:
|
||||
- "make BUILD_ARGS=sdist archive"
|
||||
- 'bash -c "cp dist/*.tar.gz ."'
|
||||
- 'bash -c "ls *.tar.gz"'
|
||||
jobs:
|
||||
- job: propose_downstream
|
||||
trigger: release
|
||||
metadata:
|
||||
dist_git_branches: fedora-development
|
||||
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
metadata:
|
||||
targets:
|
||||
- fedora-all
|
||||
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
metadata:
|
||||
targets:
|
||||
- fedora-eln
|
||||
|
||||
- job: copr_build
|
||||
trigger: commit
|
||||
metadata:
|
||||
targets:
|
||||
- fedora-rawhide
|
||||
- fedora-eln
|
||||
branch: master
|
||||
owner: "@rhinstaller"
|
||||
project: Anaconda
|
||||
preserve_project: True
|
||||
|
||||
- job: copr_build
|
||||
trigger: commit
|
||||
metadata:
|
||||
targets:
|
||||
- fedora-34
|
||||
branch: master
|
||||
owner: "@rhinstaller"
|
||||
project: Anaconda-devel
|
||||
preserve_project: True
|
||||
@@ -0,0 +1,504 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=0
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=fixme,
|
||||
invalid-name,
|
||||
missing-docstring,
|
||||
no-self-use,
|
||||
too-few-public-methods,
|
||||
too-many-arguments,
|
||||
too-many-instance-attributes,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
unnecessary-pass,
|
||||
unused-argument,
|
||||
useless-return,
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
#enable=c-extension-no-member
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
msg-template='{msg_id}({symbol}):{path}:{line},{column}: {obj}: {msg}'
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=colorized
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=no
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=99
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Format style used to check logging format string. `old` means using %
|
||||
# formatting, while `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: en_US (myspell), cs_CZ
|
||||
# (myspell), en_GB (myspell), en_AG (myspell), en_AU (myspell), en_BS
|
||||
# (myspell), en_BW (myspell), en_BZ (myspell), en_CA (myspell), en_DK
|
||||
# (myspell), en_GH (myspell), en_HK (myspell), en_IE (myspell), en_IN
|
||||
# (myspell), en_JM (myspell), en_MW (myspell), en_NA (myspell), en_NG
|
||||
# (myspell), en_NZ (myspell), en_PH (myspell), en_SG (myspell), en_TT
|
||||
# (myspell), en_ZA (myspell), en_ZM (myspell), en_ZW (myspell)..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether the implicit-str-concat-in-sequence should
|
||||
# generate a warning on implicit string concatenation in sequences defined over
|
||||
# several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=DefaultInstall,Popen,QueueFactory,TransactionSet,Namespace
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
# callbacks=cb_,
|
||||
# _cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=yes
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement.
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix,string,regsub,TERMINOS,Bastion,rexec,mock
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "BaseException, Exception".
|
||||
overgeneral-exceptions=BaseException,
|
||||
Exception
|
||||
@@ -0,0 +1,157 @@
|
||||
### GNU LESSER GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates the
|
||||
terms and conditions of version 3 of the GNU General Public License,
|
||||
supplemented by the additional permissions listed below.
|
||||
|
||||
#### 0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the
|
||||
GNU General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License, other
|
||||
than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
#### 1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
#### 2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
- a) under this License, provided that you make a good faith effort
|
||||
to ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
- b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
#### 3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from a
|
||||
header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
- a) Give prominent notice with each copy of the object code that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
- b) Accompany the object code with a copy of the GNU GPL and this
|
||||
license document.
|
||||
|
||||
#### 4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that, taken
|
||||
together, effectively do not restrict modification of the portions of
|
||||
the Library contained in the Combined Work and reverse engineering for
|
||||
debugging such modifications, if you also do each of the following:
|
||||
|
||||
- a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
- b) Accompany the Combined Work with a copy of the GNU GPL and this
|
||||
license document.
|
||||
- c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
- d) Do one of the following:
|
||||
- 0) Convey the Minimal Corresponding Source under the terms of
|
||||
this License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
- 1) Use a suitable shared library mechanism for linking with
|
||||
the Library. A suitable mechanism is one that (a) uses at run
|
||||
time a copy of the Library already present on the user's
|
||||
computer system, and (b) will operate properly with a modified
|
||||
version of the Library that is interface-compatible with the
|
||||
Linked Version.
|
||||
- e) Provide Installation Information, but only if you would
|
||||
otherwise be required to provide such information under section 6
|
||||
of the GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the Application
|
||||
with a modified version of the Linked Version. (If you use option
|
||||
4d0, the Installation Information must accompany the Minimal
|
||||
Corresponding Source and Corresponding Application Code. If you
|
||||
use option 4d1, you must provide the Installation Information in
|
||||
the manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.)
|
||||
|
||||
#### 5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the Library
|
||||
side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
- a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities, conveyed under the terms of this License.
|
||||
- b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
#### 6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
as you received it specifies that a certain numbered version of the
|
||||
GNU Lesser General Public License "or any later version" applies to
|
||||
it, you have the option of following the terms and conditions either
|
||||
of that published version or of any later version published by the
|
||||
Free Software Foundation. If the Library as you received it does not
|
||||
specify a version number of the GNU Lesser General Public License, you
|
||||
may choose any version of the GNU Lesser General Public License ever
|
||||
published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
@@ -0,0 +1,4 @@
|
||||
include LICENSE.md Makefile ChangeLog README.md
|
||||
recursive-include simpleline *.py
|
||||
recursive-include po *.po *.pot Makefile
|
||||
recursive-include tests *.py *.sh
|
||||
@@ -0,0 +1,154 @@
|
||||
# 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/>.
|
||||
|
||||
PKGNAME=simpleline
|
||||
SPECNAME=python-$(PKGNAME)
|
||||
VERSION=$(shell awk '/Version:/ { print $$2 }' $(SPECNAME).spec)
|
||||
RELEASE=$(shell awk '/Release:/ { print $$2 }' $(SPECNAME).spec | sed -e 's|%.*$$||g')
|
||||
TAG=$(PKGNAME)-$(VERSION)
|
||||
|
||||
PYTHON?=python3
|
||||
COVERAGE?=coverage3
|
||||
|
||||
# Arguments used for setup.py call for creating archive
|
||||
BUILD_ARGS ?= sdist bdist_wheel
|
||||
|
||||
# LOCALIZATION SETTINGS
|
||||
L10N_REPOSITORY ?= https://github.com/rhinstaller/python-simpleline-l10n.git
|
||||
L10N_REPOSITORY_RW ?= git@github.com:rhinstaller/python-simpleline-l10n.git
|
||||
|
||||
# Branch used in localization repository. This should be master all the time.
|
||||
GIT_L10N_BRANCH ?= master
|
||||
# Directory in localization repository specific for this branch.
|
||||
L10N_DIR ?= master
|
||||
|
||||
default: all
|
||||
|
||||
.PHONY: all
|
||||
all:
|
||||
$(MAKE) -C po
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
-rm -rf *.tar.gz simpleline/*.pyc tests/*.pyc ChangeLog dist build simpleline.egg-info
|
||||
$(MAKE) -C po clean
|
||||
$(PYTHON) setup.py -q clean --all
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "*** Running unittests ***"
|
||||
./tests/units/run_test.sh
|
||||
|
||||
.PHONY: coverage
|
||||
coverage:
|
||||
@echo "*** Running unittests with coverage ***"
|
||||
PYTHON="$(COVERAGE) run --branch" ./tests/units/run_test.sh
|
||||
$(COVERAGE) report -m --include="simpleline/*" | tee tests/coverage-report.log
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
@echo "*** Running pylint ***"
|
||||
$(PYTHON) -m pylint simpleline/ examples/*/*.py tests/units/
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
$(PYTHON) setup.py install --root=$(DESTDIR)
|
||||
$(MAKE) -C po install
|
||||
|
||||
.PHONY: ChangeLog
|
||||
ChangeLog:
|
||||
(GIT_DIR=.git git log > .changelog.tmp && mv .changelog.tmp ChangeLog; rm -f .changelog.tmp) || (touch ChangeLog; echo 'git directory not found: installing possibly empty changelog.' >&2)
|
||||
|
||||
.PHONY: tag
|
||||
tag:
|
||||
git tag -a -m "Tag as $(TAG)" -f $(TAG)
|
||||
@echo "Tagged as $(TAG)"
|
||||
|
||||
.PHONY: release
|
||||
release: tag archive
|
||||
|
||||
.PHONY: archive
|
||||
archive: po-pull ChangeLog
|
||||
$(PYTHON) setup.py $(BUILD_ARGS)
|
||||
@echo "The archive is in dist/$(PKGNAME)-$(VERSION).tar.gz"
|
||||
|
||||
.PHONY: rpmlog
|
||||
rpmlog:
|
||||
@git log --no-merges --pretty="format:- %s (%ae)" $(TAG).. |sed -e 's/@.*)/)/'
|
||||
@echo
|
||||
|
||||
.PHONY: potfile
|
||||
potfile:
|
||||
$(MAKE) -C po potfile
|
||||
|
||||
.PHONY: po-pull
|
||||
po-pull:
|
||||
TEMP_DIR=$$(mktemp --tmpdir -d $(SPECNAME)-localization-XXXXXXXXXX) && \
|
||||
git clone --depth 1 -b $(GIT_L10N_BRANCH) -- $(L10N_REPOSITORY) $$TEMP_DIR && \
|
||||
cp $$TEMP_DIR/$(L10N_DIR)/*.po ./po/ && \
|
||||
rm -rf $$TEMP_DIR
|
||||
|
||||
.PHONY: po-push
|
||||
po-push: potfile
|
||||
# This algorithm will make these steps:
|
||||
# - clone localization repository
|
||||
# - copy pot file to this repository
|
||||
# - check if pot file is changed (ignore the POT-Creation-Date otherwise it's always changed)
|
||||
# - if not changed:
|
||||
# - remove cloned repository
|
||||
# - if changed:
|
||||
# - add pot file
|
||||
# - commit pot file
|
||||
# - tell user to verify this file and push to the remote from the temp dir
|
||||
TEMP_DIR=$$(mktemp --tmpdir -d $(SPECNAME)-localization-XXXXXXXXXX) || exit 1 ; \
|
||||
git clone --depth 1 -b $(GIT_L10N_BRANCH) -- $(L10N_REPOSITORY_RW) $$TEMP_DIR || exit 2 ; \
|
||||
cp ./po/$(SPECNAME).pot $$TEMP_DIR/$(L10N_DIR)/ || exit 3 ; \
|
||||
pushd $$TEMP_DIR/$(L10N_DIR) ; \
|
||||
git difftool --trust-exit-code -y -x "diff -u -I '^\"POT-Creation-Date: .*$$'" HEAD ./$(SPECNAME).pot &>/dev/null ; \
|
||||
if [ $$? -eq 0 ] ; then \
|
||||
popd ; \
|
||||
echo "Pot file is up to date" ; \
|
||||
rm -rf $$TEMP_DIR ; \
|
||||
else \
|
||||
git add ./$(SPECNAME).pot && \
|
||||
git commit -m "Update $(SPECNAME).pot" && \
|
||||
popd && \
|
||||
echo "Pot file updated for the localization repository $(L10N_REPOSITORY)" && \
|
||||
echo "Please confirm changes and push:" && \
|
||||
echo "$$TEMP_DIR" ; \
|
||||
fi ;
|
||||
|
||||
.PHONY: bumpver
|
||||
bumpver: po-push
|
||||
read -p "Please see the above message. Verify and push localization commit. Press anything to continue." -n 1 -r
|
||||
|
||||
@NEWSUBVER=$$((`echo $(VERSION) |cut -d . -f 2` + 1)) ; \
|
||||
NEWVERSION=`echo $(VERSION).$$NEWSUBVER |cut -d . -f 1,3` ; \
|
||||
DATELINE="* `LC_ALL=C.UTF-8 date "+%a %b %d %Y"` `git config user.name` <`git config user.email`> - $$NEWVERSION-1" ; \
|
||||
cl=`grep -n %changelog $(SPECNAME).spec |cut -d : -f 1` ; \
|
||||
tail --lines=+$$(($$cl + 1)) $(SPECNAME).spec > speclog ; \
|
||||
(head -n $$cl $(SPECNAME).spec ; echo "$$DATELINE" ; make --quiet rpmlog 2>/dev/null ; echo ""; cat speclog) > $(SPECNAME).spec.new ; \
|
||||
mv $(SPECNAME).spec.new $(SPECNAME).spec ; rm -f speclog ; \
|
||||
sed -i "s/Version: $(VERSION)/Version: $$NEWVERSION/" $(SPECNAME).spec ; \
|
||||
sed -i "s/version='$(VERSION)'/version='$$NEWVERSION'/" setup.py
|
||||
|
||||
.PHONY: pypi-upload
|
||||
pypi-upload:
|
||||
$(PYTHON) -m twine upload dist/*
|
||||
|
||||
.PHONY: ci
|
||||
ci: check test
|
||||
@@ -0,0 +1,48 @@
|
||||
[](https://copr.fedorainfracloud.org/coprs/g/rhinstaller/Anaconda/package/python-simpleline/)
|
||||
[](https://translate.fedoraproject.org/engage/python-simpleline/?utm_source=widget)
|
||||
|
||||
Python Simpleline
|
||||
=================
|
||||
|
||||
This is a text UI framework originally which was a part of the
|
||||
[Anaconda](https://github.com/rhinstaller/anaconda) installer project.
|
||||
|
||||
This UI is simple and easy to use. It is designed to be used with line-based
|
||||
machines and tools (e.g. serial console) so that every new line is appended
|
||||
to the bottom of the screen. Printed lines are never rewritten!
|
||||
|
||||
How to
|
||||
======
|
||||
|
||||
[Link](https://python-simpleline.readthedocs.io) to the documentation.
|
||||
|
||||
Another way to learn how to use this library is to view the
|
||||
[examples directory](https://github.com/rhinstaller/python-simpleline/tree/master/examples).
|
||||
Examples can be run without installing Simpleline to the system. You can test
|
||||
them by running the script `run_example.sh`.
|
||||
|
||||
1. `cd examples`
|
||||
2. `./run_example.sh example_folder_name`
|
||||
|
||||
For example:
|
||||
`./run_example.sh basic`
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
This is a Python3-only project. This code should not be difficult to migrate to
|
||||
Python2. However, there is no need from the community, so it is only compatible
|
||||
with Python3 at the moment.
|
||||
No special libraries are required to use this library. If you want to use glib
|
||||
event loop instead of the original one you need to install glib and Python3
|
||||
gobject introspection.
|
||||
|
||||
If you want to run tests (`make ci`), you need to install
|
||||
[Pocketlint](https://github.com/rhinstaller/pocketlint) and glib with gobject
|
||||
introspection for Python3.
|
||||
|
||||
Note
|
||||
====
|
||||
|
||||
Thanks to Martin Sivák for the original Anaconda project. It was really nice
|
||||
starting place for the new Simpleline's form.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build-3
|
||||
SPHINXPROJ = Simpleline
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
@@ -0,0 +1,22 @@
|
||||
.. _public_api_label:
|
||||
|
||||
Public API
|
||||
==========
|
||||
|
||||
API listed here is the public API. Developers shouldn't use anything which is not mentioned here!
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Contents:
|
||||
|
||||
App <api/app>
|
||||
UIScreen <api/screen>
|
||||
Screen Handling <api/screen_handling>
|
||||
Widgets <api/widgets>
|
||||
Containers <api/containers>
|
||||
Prompt <api/prompt>
|
||||
Advanced Widgets <api/adv_widgets>
|
||||
Event loops <api/event_loops>
|
||||
Signals <api/signals>
|
||||
Errors <api/errors>
|
||||
Advanced Input <api/advanced_input>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
Advanced Widgets
|
||||
================
|
||||
|
||||
Advanced widgets are :class:`screens <simpleline.render.screen.UIScreen>` which can be used for a
|
||||
specific purpose. For example, reading input from the user and testing acceptance conditions on this
|
||||
input, or asking the user a yes/no question.
|
||||
|
||||
Advanced widget classes
|
||||
-----------------------
|
||||
|
||||
.. automodule:: simpleline.render.adv_widgets
|
||||
:members:
|
||||
:show-inheritance:
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
Advanced Input
|
||||
==============
|
||||
|
||||
.. automodule:: simpleline.input.input_handler
|
||||
|
||||
.. WARNING::
|
||||
This section contains advanced input techniques which may lead to buggy code in your
|
||||
program if they are not used correctly.
|
||||
|
||||
The default technique to get user input is already described in :ref:`UIScreen <uiscreen_label>`,
|
||||
and that should be the preferred way to obtain user input. However, if the default technique is
|
||||
not enough for your situation then read the text below to find out how to implement custom input.
|
||||
|
||||
Input handler classes
|
||||
---------------------
|
||||
Input handler classes are classes created to obtain user input. If required
|
||||
:class:`InputHandler` can block an application until user input is received.
|
||||
|
||||
This class can be instantiated everywhere in the code and used to ask for user input::
|
||||
|
||||
handler = InputHandler()
|
||||
handler.get_input("Shut up and give me your input:")
|
||||
handler.wait_for_input()
|
||||
if handler.input_successful:
|
||||
user_input = handler.value
|
||||
|
||||
The :meth:`InputHandler.wait_on_input` method will block code processing until user input is
|
||||
received. Input should always be checked before processing.
|
||||
|
||||
In case some other work needs to be done before user input is received, then pass
|
||||
a callback to the constructor of the :class:`InputHandler` class and do not use
|
||||
the :meth:`InputHandler.wait_on_input` method. However, the callback is using
|
||||
the :ref:`event loop<event_loops_label>` so it won't be called until event loop
|
||||
processing is active. If :ref:`concurrent input<concurrent_input_label>` is used then this
|
||||
callback might never get called!
|
||||
|
||||
If what a user types must not be displayed, then the :class:`PasswordInputHandler` class
|
||||
should be used. It shares most of its implementation with the :class:`InputHandler` but overrides
|
||||
how to obtain the code. For more info look at the :class:`PasswordInputHandler` class
|
||||
documentation.
|
||||
|
||||
.. _concurrent_input_label:
|
||||
|
||||
Concurrent input
|
||||
----------------
|
||||
|
||||
Concurrent input is something which should be avoided. It drags unexpected behavior into an
|
||||
application and is hard to debug. However, there could be an instance when user input is required
|
||||
immediately, even when an application is already waiting for other input.
|
||||
|
||||
By default, every attempt for concurrent input will raise an exception and kill the application to
|
||||
prevent unexpected behavior. In order to allow for concurrent input, the
|
||||
:attr:`InputHandler.skip_concurrency_check` property must be set. After this property is disabled
|
||||
for the :class:`InputHandler` instance, then the handler instance then it can support
|
||||
concurrent input.
|
||||
|
||||
The last registered concurrent input will result in dropping all other waiting inputs -- even other
|
||||
waiting inputs with :attr:`InputHandler.skip_concurrency_check` will be dropped. The dropped
|
||||
waiting inputs will get a failed input signal to unblock :meth:`InputHandler.wait_on_input`
|
||||
methods.
|
||||
|
||||
|
||||
Creating a custom InputHandler
|
||||
------------------------------
|
||||
|
||||
If the :class:`InputHandler` class or the :class:`PasswordInputHandler` class is not enough,
|
||||
developers can create their own handler. The structure of an input handler is based on two
|
||||
classes. First is a handler itself, and second is the request object it creates.
|
||||
|
||||
The handler class is used as an interface for the rest of the application. It also creates
|
||||
the requester instance. The requester object is a low-level implementation of obtaining user input.
|
||||
The requester object has to have the ``get_input`` method and should contain the ``text_prompt``
|
||||
method. The ``get_input`` method is called in a separate thread and should prompt the user for
|
||||
input. The ``text_prompt`` method is required mainly for concurrent input, and it returns
|
||||
a string representation of the prompt.
|
||||
|
||||
For more details, please look at the implementation of the :class:`InputHandler` class.
|
||||
|
||||
|
||||
InputHandler class
|
||||
------------------
|
||||
|
||||
.. autoclass:: InputHandler
|
||||
:members:
|
||||
|
||||
PasswordInputHandler class
|
||||
--------------------------
|
||||
|
||||
.. autoclass:: PasswordInputHandler
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
App
|
||||
===
|
||||
|
||||
.. automodule:: simpleline
|
||||
|
||||
The App class is the heart of Simpleline. It holds the :ref:`event loop <event_loops_label>`
|
||||
instance and scheduler which are used by Simpleline. Both can be replaced if needed in the
|
||||
initialization phase by calling the :meth:`App.initialize` method. However, by replacing the
|
||||
scheduler you are replacing most of the logic in Simpleline. Replacing the scheduler is not
|
||||
supported, since it is currently not a part of the public API.
|
||||
|
||||
All Simpleline applications must be run by the :meth:`App.run` method. This method will start
|
||||
the event loop instance created in the initialization process.
|
||||
|
||||
If a reaction on application quit is required, please set
|
||||
:meth:`set_quit_callback <event_loop.AbstractEventLoop.set_quit_callback>` in the used event loop.
|
||||
This can be done by::
|
||||
|
||||
loop = App.get_event_loop()
|
||||
loop.set_quit_callback(callback_function)
|
||||
|
||||
**Do not instantiate the** :class:`App` **class!** It is designed to be used in a purely static way.
|
||||
|
||||
|
||||
Application configuration
|
||||
-------------------------
|
||||
|
||||
Configuration of the application is saved in the
|
||||
:class:`GlobalConfiguration <global_configuration.GlobalConfiguration>` class. This class can be
|
||||
created before :meth:`App.initialize` is called and passed in as a
|
||||
parameter. This way the same configuration can be used between re-initialization of the
|
||||
application. The configuration can easily be changed, even while an application is running,
|
||||
by setting desired properties.
|
||||
|
||||
Look at the :class:`GlobalConfiguration <global_configuration.GlobalConfiguration>` to find out
|
||||
all the configuration possibilities.
|
||||
|
||||
App class
|
||||
---------
|
||||
|
||||
.. autoclass:: App
|
||||
:members:
|
||||
|
||||
GlobalConfiguration class
|
||||
-------------------------
|
||||
|
||||
.. autoclass:: simpleline.global_configuration.GlobalConfiguration
|
||||
:members:
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
.. _containers_label:
|
||||
|
||||
Containers
|
||||
==========
|
||||
|
||||
Containers are structures to hold widgets and handle automatic positioning. Containers are
|
||||
essentially widgets containing other widgets. Below is a collection of default containers which
|
||||
can position :ref:`widgets <widgets_label>`, but they can do more (e.g. handle user input).
|
||||
Recursive composition of containers is supported.
|
||||
|
||||
Customized containers can also be created. See the :ref:`creating_a_custom_container_label`
|
||||
section.
|
||||
|
||||
Container classes
|
||||
-----------------
|
||||
|
||||
.. automodule:: simpleline.render.containers
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. _creating_a_custom_container_label:
|
||||
|
||||
Creating a custom container
|
||||
---------------------------
|
||||
|
||||
If an existing container is missing a required feature, a new, customized container can be
|
||||
created based on the :class:`Container` class. Container creation is essentially the same as
|
||||
:ref:`custom widget creation <create_custom_widget_label>` because containers are based on widgets.
|
||||
The main difference is that containers are working with widgets added by a developer when using
|
||||
the container. As an example, every :class:`UIScreen <simpleline.render.screen.UIScreen>` has a
|
||||
:class:`WindowContainer`, and this is used as the main rendering point.
|
||||
|
||||
To create a customized container, the :meth:`Container.render` method should be overridden and
|
||||
the positioning of widgets and containers should be done here. It can even enhance these widgets,
|
||||
for example, by adding numbering (this is done in :class:`ListRowContainer` or
|
||||
:class:`ListColumnContainer`). The :meth:`Container.render` method should call the
|
||||
:meth:`Container.draw` method. The :meth:`Container.draw` method should be called for every
|
||||
widget placed. For a better understanding please refer to the existing implementation.
|
||||
|
||||
Base container class
|
||||
--------------------
|
||||
|
||||
.. autoclass:: Container
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,39 @@
|
||||
Errors module
|
||||
=============
|
||||
|
||||
.. automodule:: simpleline.errors
|
||||
|
||||
Collection of generic exception classes used everywhere in the project. The most important one is
|
||||
:class:`SimplelineError`, which is the base exception for all the other exceptions used in
|
||||
the Simpleline project.
|
||||
|
||||
.. autoexception:: SimplelineError
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Render exceptions
|
||||
-----------------
|
||||
|
||||
Exceptions used for rendering errors.
|
||||
|
||||
|
||||
.. automodule:: simpleline.render
|
||||
|
||||
.. autoexception:: RenderError
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
.. autoexception:: RenderUnexpectedError
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Event loop exceptions
|
||||
---------------------
|
||||
|
||||
Exceptions used for errors in event loops.
|
||||
|
||||
.. automodule:: simpleline.event_loop
|
||||
|
||||
.. autoexception:: simpleline.event_loop.ExitMainLoop
|
||||
:members:
|
||||
:show-inheritance:
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
.. _event_loops_label:
|
||||
|
||||
Event Loops
|
||||
===========
|
||||
|
||||
.. currentmodule:: simpleline.event_loop
|
||||
|
||||
Event loops are the heart of Simpleline. Every event loop is based on the
|
||||
:class:`AbstractEventLoop`, and they all work with :ref:`signals <signals_label>`.
|
||||
A signal is a message passed to the loop containing some information. Signals are passed to
|
||||
an event loop by calling :meth:`AbstractEventLoop.enqueue_signal`. These signals are then
|
||||
processed by calling :meth:`AbstractEventLoop.process_signals`. This method can be called by
|
||||
an application developer manually or by the :meth:`AbstractEventLoop.run` method, which is
|
||||
called by :meth:`App.run() <simpleline.App.run>` to start the Simpleline-based application.
|
||||
When a signal is processed, all handlers attached to this signal are called. Signal handler
|
||||
assignment is done by the :meth:`AbstractEventLoop.register_signal_handler` method.
|
||||
|
||||
Event loops can also be started recursively by :meth:`AbstractEventLoop.execute_new_loop`.
|
||||
The old event loop is waiting for this event loop to stop. New loop execution is mandatory for
|
||||
modal screens to work, since they can't be interrupted by other screens. This new event
|
||||
loop is terminated by closing the last screen in the event loop or by calling
|
||||
the :meth:`AbstractEventLoop.close_loop` method.
|
||||
|
||||
The last event loop should be terminated by closing the last screen in the screen stack or by
|
||||
calling :meth:`AbstractEventLoop.close_loop`. In case a fatal error occurs the
|
||||
:meth:`AbstractEventLoop.force_quit` method can be used to immediately kill the loop.
|
||||
|
||||
If a reaction on quitting the application (closing the last event loop) is required, the quit
|
||||
callback can be used. The quit callback can be set by
|
||||
the :meth:`AbstractEventLoop.set_quit_callback` method.
|
||||
|
||||
The following event loops are supported by Simpleline, but you can also
|
||||
:ref:`Create_your_own_loop_label` :
|
||||
|
||||
* :ref:`MainLoop_label`
|
||||
* :ref:`GLib_Event_loop_label`
|
||||
|
||||
.. _MainLoop_label:
|
||||
|
||||
Main Loop
|
||||
---------
|
||||
|
||||
The main loop is the default event loop for Simpleline projects. The benefit of using
|
||||
:class:`MainLoop <main_loop.MainLoop>` is that it isn't necessary to have any dependencies on
|
||||
other libraries. It is a lightweight event loop implemented completely in Python.
|
||||
|
||||
.. autoclass:: simpleline.event_loop.main_loop.MainLoop
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. _GLib_Event_loop_label:
|
||||
|
||||
GLib Event loop
|
||||
---------------
|
||||
|
||||
The GLib event loop was added in order to utilize existing event loops used by other libraries,
|
||||
for example, DBus connections. Simpleline with this loop should have the same behavior as with
|
||||
the :ref:`MainLoop_label`.
|
||||
|
||||
To use this loop you need to set it via the :class:`App <simpleline.App>` class::
|
||||
|
||||
# Create Glib event loop.
|
||||
glib_loop = GLibEventLoop()
|
||||
|
||||
# Use glib event loop instead of the original one.
|
||||
# Everything else should behave the same as with the original Simpleline loop.
|
||||
App.initialize(event_loop=glib_loop)
|
||||
|
||||
The GLib loop can be accessed by the
|
||||
:attr:`GLibEventLoop.active_main_loop <simpleline.event_loop.glib_event_loop.GLibEventLoop.active_main_loop>`
|
||||
property, or by getting the default loop from GLib directly
|
||||
`GLib.MainLoop() <https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html>`_.
|
||||
|
||||
.. autoclass:: simpleline.event_loop.glib_event_loop.GLibEventLoop
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. _Create_your_own_loop_label:
|
||||
|
||||
Create your own loop
|
||||
--------------------
|
||||
|
||||
If new loop support is required, it should inherit from :class:`AbstractEventLoop` and
|
||||
implement the same behavior as the :ref:`MainLoop_label`. You can use existing tests from the
|
||||
event loops to start. If the new loop is stable enough, pull requests are always welcome at
|
||||
`Simpleline repository <https://github.com/rhinstaller/python-simpleline>`_.
|
||||
|
||||
.. autoclass:: simpleline.event_loop.AbstractEventLoop
|
||||
:members:
|
||||
:inherited-members:
|
||||
@@ -0,0 +1,23 @@
|
||||
.. _prompt_label:
|
||||
|
||||
Prompt
|
||||
======
|
||||
|
||||
.. automodule:: simpleline.render.prompt
|
||||
|
||||
Class for prompting a user for input. New user options can be added by :meth:`Prompt.add_option`,
|
||||
removed by :meth:`Prompt.remove_option` or updated by :meth:`Prompt.update_option`.
|
||||
A message for the user can also be set by the :meth:`Prompt.set_message` method.
|
||||
|
||||
This class is used in the :class:`UIScreen <simpleline.render.screen.UIScreen>` class. The default
|
||||
instance always handles *r* (refresh), *c* (continue) and *q* (quit) and is created in the
|
||||
:meth:`UIScreen.prompt() <simpleline.render.screen.UIScreen.prompt>` method. To create your own
|
||||
custom prompt please override
|
||||
the :meth:`UIScreen.prompt() <simpleline.render.screen.UIScreen.prompt>` method.
|
||||
|
||||
Prompt class
|
||||
------------
|
||||
|
||||
.. autoclass:: Prompt
|
||||
:members:
|
||||
:inherited-members:
|
||||
@@ -0,0 +1,126 @@
|
||||
.. _uiscreen_label:
|
||||
|
||||
UIScreen
|
||||
========
|
||||
|
||||
.. automodule:: simpleline.render.screen
|
||||
|
||||
The base class for creating a new screen. :class:`UIScreen` is used for any user interaction.
|
||||
:class:`UIScreen` uses :ref:`containers <containers_label>` and :ref:`widgets <widgets_label>`
|
||||
to present information to a user, and the :meth:`UIScreen.input` method to get input from a user
|
||||
to an application. Screens are pushed to the screen stack, which are then used to
|
||||
communicate with a user. See the :ref:`screen handling <screen_handling_label>` section to find
|
||||
out more.
|
||||
|
||||
The methods :meth:`UIScreen.redraw`, :meth:`UIScreen.close`, :meth:`UIScreen.emit` and
|
||||
:meth:`UIScreen.create_and_emit` are asynchronous. These methods will create a signal which
|
||||
is passed to the event loop for later processing.
|
||||
|
||||
**Beware**, methods :meth:`UIScreen.redraw` and :meth:`UIScreen.close` can lead to unexpected
|
||||
behavior when multiple instances of these signals are emitted (methods are called multiple times).
|
||||
This can even crash your application.
|
||||
|
||||
Lifecycle
|
||||
---------
|
||||
|
||||
Every :class:`UIScreen` has a distinct lifecycle: *uninitialized*, *initialized*, *draw*,
|
||||
*process input* and *closed*. If the screen has already been initialized
|
||||
(it was *drawn* to a monitor) and will be shown again, then the screen skips
|
||||
the *uninitialized* stage. The screen can cycle between *draw* and *process input* stages by
|
||||
calling the :meth:`UIScreen.redraw` method after processing user input.
|
||||
|
||||
In case :meth:`UIScreen.redraw` won't be called and no new screen is pushed, or this screen
|
||||
wasn't closed, then an application will stay in an infinite loop waiting for something to happen.
|
||||
This is correct behavior because there could be something in an event loop which will
|
||||
call :meth:`UIScreen.redraw` later.
|
||||
|
||||
Rendering widgets
|
||||
-----------------
|
||||
|
||||
The :meth:`UIScreen.refresh` method is the most important part of the :class:`UIScreen`.
|
||||
It contains preparations for rendering (creating widgets and adding them to containers).
|
||||
The :meth:`UIScreen.refresh` method will be called before anything is drawn on a monitor.
|
||||
|
||||
The :attr:`UIScreen.window` attribute, which is the
|
||||
:class:`WindowContainer <simpleline.render.containers.WindowContainer>` instance, contains
|
||||
all items (widgets, containers) which are to be rendered by the screen.
|
||||
A new :class:`WindowContainer <simpleline.render.containers.WindowContainer>` is created in the
|
||||
:meth:`UIScreen.refresh` method for every screen redraw. The :attr:`UIScreen.title` attribute is
|
||||
passed to the :attr:`WindowContainer.title <simpleline.render.containers.WindowContainer.title>`
|
||||
property, and a developer can add :ref:`widgets <widgets_label>` and other
|
||||
:ref:`containers <containers_label>` to present items to a user by calling
|
||||
:meth:`WindowContainer.add() <simpleline.render.containers.WindowContainer.add>` or
|
||||
:meth:`WindowContainer.add_with_separator() <simpleline.render.containers.WindowContainer.add_with_separator>`.
|
||||
Multiple items can be added by calling these methods repeatedly.
|
||||
|
||||
When everything is prepared properly in the :meth:`UIScreen.refresh` method, it needs to be drawn
|
||||
on the monitor for a user. This is handled by the :meth:`UIScreen.show_all` method. This method
|
||||
works automatically, but it could also be useful for a developer, especially when the screen will
|
||||
not process input. In this case, the :meth:`UIScreen.input` method is not called at all.
|
||||
Developers can override this method and call the parent class's :meth:`UIScreen.show_all`,
|
||||
which will handle drawing the screen and any additional processing.
|
||||
|
||||
Redrawing the screen can be invoked by the :meth:`UIScreen.redraw` method. However, this is
|
||||
not processed immediately. Instead it will be added to the event loop and processed later, when the
|
||||
loop is idle. Beware, after every :meth:`redraw <UIScreen.redraw>` call the input is processed
|
||||
if not disabled, so if multiple redraw signals are emitted, than the application will
|
||||
crash.
|
||||
|
||||
The :meth:`UIScreen.redraw` method is also invoked when a screen is
|
||||
:ref:`pushed <push_screen_label>` to the stack.
|
||||
|
||||
User input processing
|
||||
---------------------
|
||||
|
||||
If the screen shouldn't process user input, the :attr:`UIScreen.input_required` property needs
|
||||
to be set to `False`. `True` is the default value for this property.
|
||||
|
||||
After everything is printed to a monitor, the :class:`UIScreen` will wait for user input.
|
||||
For this purpose there is :meth:`UIScreen.input`, which is called when a user passes string input
|
||||
to a screen. The screen needs to react upon user input and return one of the options from the
|
||||
:class:`InputState` enum or the user input string.
|
||||
|
||||
To accept the user input, :attr:`InputState.PROCESSED`, :attr:`InputState.PROCESSED_AND_REDRAW` or
|
||||
:attr:`InputState.PROCESSED_AND_CLOSE` should be returned. Addition to accepting user input the
|
||||
:attr:`InputState.PROCESSED_AND_REDRAW` value will also redraw active screen and
|
||||
:attr:`InputState.PROCESSED_AND_CLOSE` will close active screen. However, if
|
||||
:attr:`InputState.PROCESSED` is used then the developer is responsible for not ending in frozen
|
||||
application. The :meth:`UIScreen.refresh` or the :meth:`UIScreen.close` methods must be called
|
||||
manually.
|
||||
|
||||
In case the user input is invalid, the :attr:`InputState.DISCARDED` value should be returned.
|
||||
This will reject the user input and wait for another attempt. The :class:`UIScreen.refresh`
|
||||
method will be called after 5 rejections and show the screen output again.
|
||||
|
||||
If the user input string is returned it will be checked for
|
||||
options of the :class:`Prompt <simpleline.render.prompt.Prompt>` instance which can either
|
||||
close the screen, refresh the screen (this will call :meth:`UIScreen.refresh`)
|
||||
or quit the application.
|
||||
|
||||
Closing screens
|
||||
---------------
|
||||
|
||||
There are several ways to close a screen. One is by calling :meth:`UIScreen.close` or
|
||||
by pressing *c* to continue (with the default
|
||||
:class:`Prompt <simpleline.render.prompt.Prompt>` class). When the screen is closed
|
||||
the next screen on the stack will be shown. A screen can also be
|
||||
:ref:`replaced <replace_screen_label>`. Then the original screen is removed from the stack
|
||||
without closing a screen. If a reaction on closing a screen is required then the
|
||||
:meth:`UIScreen.closed` callback should be overridden.
|
||||
|
||||
**Beware**, when calling :meth:`UIScreen.close` multiple times it will close multiple screens.
|
||||
This is because the close signal will always close the top screen on the screen stack.
|
||||
|
||||
UIScreen class
|
||||
--------------
|
||||
|
||||
.. autoclass:: UIScreen
|
||||
:members:
|
||||
:inherited-members:
|
||||
|
||||
InputState enum
|
||||
---------------
|
||||
|
||||
.. autoclass:: InputState
|
||||
:members:
|
||||
:undoc-members:
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
.. _screen_handling_label:
|
||||
|
||||
Screen Handling
|
||||
===============
|
||||
|
||||
.. automodule:: simpleline.render.screen_handler
|
||||
|
||||
The :class:`ScreenHandler` class is used to schedule a
|
||||
:class:`UIScreen <simpleline.render.screen.UIScreen>` to the screen stack.
|
||||
|
||||
Screen handling is an important part of using the Simpleline library, and it is recommended
|
||||
for a developer to get familiar with this principle. Screen handling is the gateway by which
|
||||
a developer manages the stack, by either adding or removing screens from it.
|
||||
There are many ways to add a screen to the stack. To remove a screen from the stack, the screen
|
||||
must be closed, or it can be replaced by another screen.
|
||||
To close a :class:`UIScreen <simpleline.render.screen.UIScreen>` a developer should call
|
||||
the :meth:`UIScreen.close() <simpleline.render.screen.UIScreen.close()>`
|
||||
method. A screen can also be closed when a user presses `c` (which can be disabled). This will
|
||||
close the screen automatically. When a screen is closed, the next screen on the top of
|
||||
the stack will be rendered. If the stack is empty then the application will close.
|
||||
|
||||
**Do not instantiate the** :class:`ScreenHandler` **class!** All methods in the
|
||||
:class:`ScreenHandler` class are class methods so the :class:`ScreenHandler` shouldn't be
|
||||
instantiated at all.
|
||||
|
||||
The following operations can be used to schedule a screen.
|
||||
|
||||
Schedule screen
|
||||
---------------
|
||||
|
||||
To schedule a screen use the :meth:`ScreenHandler.schedule_screen` method.
|
||||
Scheduling a screen should be used on the first screen in your application before starting the
|
||||
event loop. The screen is added to the bottom of the screen stack, and it will be visible as
|
||||
the last screen in a stack.
|
||||
|
||||
This is the only way to add a screen to the screen stack without emitting a redraw call.
|
||||
|
||||
.. _push_screen_label:
|
||||
|
||||
Push screen
|
||||
-----------
|
||||
|
||||
To push a screen to the stack use the :meth:`ScreenHandler.push_screen` method.
|
||||
Pushing a screen to the stack will place a screen on top of the stack, so it will be
|
||||
drawn on the next redraw call. The original screen will remain on the stack, so after the new
|
||||
screen is closed, the original screen will be rendered again. If you need to avoid this behavior
|
||||
please close the active screen before pushing a new screen to the stack.
|
||||
|
||||
The redraw signal will be emitted automatically when a screen is pushed.
|
||||
|
||||
.. _push_screen_modal_label:
|
||||
|
||||
Push screen as modal
|
||||
--------------------
|
||||
|
||||
To push a modal screen to the stack use the :meth:`ScreenHandler.push_screen_modal` method.
|
||||
This behaves the same as :ref:`push_screen_label` with one important difference. The pushed screen
|
||||
will act as a modal screen. A modal screen has its own event loop, so event processing of the old
|
||||
loop is blocked until the modal screen is closed. The code processing is also blocked by the
|
||||
:meth:`ScreenHandler.push_screen_modal` method call.
|
||||
|
||||
The :meth:`UIScreen.redraw() <simpleline.render.screen.UIScreen.redraw>` method may be
|
||||
required if the original screen was already drawn before invoking the
|
||||
:meth:`ScreenHandler.push_screen_method` method.
|
||||
|
||||
The redraw signal will be emitted automatically when a screen is pushed.
|
||||
|
||||
.. _replace_screen_label:
|
||||
|
||||
Replace screen
|
||||
--------------
|
||||
|
||||
Replace an existing screen with a new screen. This behaves like :ref:`push_screen_label` but it
|
||||
replaces the original screen, so there is no need to close the original screen.
|
||||
|
||||
The redraw signal will be emitted automatically when a screen is replaced.
|
||||
|
||||
ScreenHandler class
|
||||
-------------------
|
||||
|
||||
.. autoclass:: ScreenHandler
|
||||
:members:
|
||||
@@ -0,0 +1,22 @@
|
||||
.. _signals_label:
|
||||
|
||||
Signals
|
||||
=======
|
||||
|
||||
This is a collection of signals that can be used in the
|
||||
:class:`event loop <simpleline.event_loop.AbstractEventLoop>`.
|
||||
|
||||
.. automodule:: simpleline.event_loop.signals
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
Creating custom signals
|
||||
-----------------------
|
||||
|
||||
New signals can be created by subclassing an existing signal class or
|
||||
:class:`AbstractSignal <simpleline.event_loop.AbstractSignal>`
|
||||
|
||||
.. autoclass:: simpleline.event_loop.AbstractSignal
|
||||
:members:
|
||||
:inherited-members:
|
||||
@@ -0,0 +1,44 @@
|
||||
.. _widgets_label:
|
||||
|
||||
Widgets
|
||||
=======
|
||||
|
||||
.. currentmodule:: simpleline.render.widgets
|
||||
|
||||
Widgets are the basic units to render items on a
|
||||
:class:`screen <simpleline.render.screen.UIScreen>`. Widgets can wrap a common text
|
||||
(:class:`TextWidget`) or create empty lines (:class:`SeparatorWidget`). They can also be more
|
||||
complex structures (:class:`CheckboxWidget`). A new widget can also be created.
|
||||
See :ref:`create_custom_widget_label`.
|
||||
|
||||
These widgets should be used in :ref:`containers_label`.
|
||||
|
||||
Widgets classes
|
||||
---------------
|
||||
|
||||
.. automodule:: simpleline.render.widgets
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
.. _create_custom_widget_label:
|
||||
|
||||
Creating a custom widget
|
||||
------------------------
|
||||
|
||||
To create a custom widget you should subclass the :class:`Widget` class. The most important method
|
||||
for creating a customized widget is :meth:`Widget.render`. This method is responsible for
|
||||
presenting a user textual information.
|
||||
|
||||
If the new widget is composed of other widgets the :meth:`Widget.draw` method should be
|
||||
called on every widget used. The :meth:`Widget.write` method should be called if the input
|
||||
is a string. These calls can be repeated multiple times. For an example please look at the
|
||||
existing implementation.
|
||||
|
||||
Base Widget class
|
||||
-----------------
|
||||
|
||||
.. autoclass:: Widget
|
||||
:members:
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Simpleline documentation build configuration file, created by
|
||||
# sphinx-quickstart on Fri Nov 3 11:00:41 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.imgmath']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'Simpleline'
|
||||
copyright = '2017, Jiri Konecny' # pylint: disable=redefined-builtin
|
||||
author = 'Jiri Konecny'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
|
||||
|
||||
def get_version():
|
||||
"""Read version from ../python-simpleline.spec ."""
|
||||
import re
|
||||
version_re = re.compile(r"^Version: *([\d.]+)$")
|
||||
with open("../python-simpleline.spec", "r") as f:
|
||||
for line in f:
|
||||
m = version_re.match(line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
|
||||
|
||||
# The short X.Y version.
|
||||
version = get_version()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Simplelinedoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'Simpleline.tex', 'Simpleline Documentation',
|
||||
'Jiri Konecny', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'simpleline', 'Simpleline Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Simpleline', 'Simpleline Documentation',
|
||||
author, 'Simpleline', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
# -- Options for Epub output ----------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
epub_author = author
|
||||
epub_publisher = author
|
||||
epub_copyright = copyright
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#
|
||||
# epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ['search.html']
|
||||
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'https://docs.python.org/3': None}
|
||||
|
||||
|
||||
# -- Mock missing stuff in readthedocs ------------------------------------
|
||||
|
||||
|
||||
class Mock(MagicMock):
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
return MagicMock()
|
||||
|
||||
|
||||
MOCK_MODULES = ['gi', 'gi.repository']
|
||||
sys.modules.update((mod_name, Mock()) for mod_name in MOCK_MODULES)
|
||||
@@ -0,0 +1,176 @@
|
||||
.. _guide_label:
|
||||
|
||||
Guide to Simpleline
|
||||
===================
|
||||
|
||||
Simpleline is a text user interface framework written completely in Python 3 with a possibility to
|
||||
have :ref:`non-python event loops <event_loops_label>`. With the exception of optional event loops,
|
||||
Simpleline has almost no dependency on external libraries.
|
||||
|
||||
This UI is simple and easy to use. It is designed to be used with line-based machines and tools
|
||||
(e.g. serial console) so that every new line is appended to the bottom of the screen.
|
||||
Printed lines are never rewritten!
|
||||
|
||||
Basic components
|
||||
----------------
|
||||
|
||||
For every application, the following parts are always required.
|
||||
|
||||
* The :class:`App <simpleline.App>` static class to initialize and run application.
|
||||
* The :class:`ScreenHandler <simpleline.render.screen_handler.ScreenHandler>` static class for
|
||||
scheduling screens.
|
||||
* The :class:`UIScreen <simpleline.render.screen.UIScreen>` based classes to create screens which
|
||||
will form the application.
|
||||
* :ref:`Widgets <widgets_label>` to show anything on the screens.
|
||||
* :ref:`Containers <containers_label>` to position widgets on the screen.
|
||||
|
||||
Look at the next section to see how everything fits together.
|
||||
|
||||
How to create a simple application
|
||||
----------------------------------
|
||||
|
||||
.. currentmodule:: simpleline.render.screen
|
||||
|
||||
Interaction with a user is necessary to have a useful UI framework. To show anything to a user the
|
||||
:class:`UIScreen` class must be used. So we will subclass this class to create our screen and set
|
||||
a title for it::
|
||||
|
||||
class DividerScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
# Set title of the screen.
|
||||
super().__init__(title=u"Divider")
|
||||
self._message = 0
|
||||
|
||||
The ``self._message`` variable will be used later to show results to the user.
|
||||
|
||||
The screen's main purpose is to present content to a user. For this we need widgets and containers.
|
||||
|
||||
The :attr:`UIScreen.window` attribute is the most important part of the screen for rendering.
|
||||
It contains the :class:`WindowContainer <simpleline.render.containers.WindowContainer>`
|
||||
container which is created and filled up by the :meth:`UIScreen.refresh` method.
|
||||
Everything added to this container is printed to the monitor.
|
||||
We should override the :meth:`UIScreen.refresh` method and call the parent's version to
|
||||
prepare the container. Then we can add :ref:`widgets <widgets_label>` to the container.
|
||||
The screen will continue like this:
|
||||
|
||||
.. code-block:: python
|
||||
:dedent: 0
|
||||
|
||||
def refresh(self, args=None):
|
||||
# Fill the self.window attribute by the WindowContainer and set screen title as header.
|
||||
super().refresh()
|
||||
|
||||
widget = TextWidget("Result: " + str(self._message))
|
||||
self.window.add_with_separator(widget)
|
||||
|
||||
The :meth:`WindowContainer.add_with_separator() <simpleline.render.containers.WindowContainer.add_with_separator>`
|
||||
method will print a blank line after the
|
||||
:class:`TextWidget's <simpleline.render.widgets.TextWidget>` text.
|
||||
|
||||
Now the user has the header and result printed on the screen but it would be nice to give them a
|
||||
hint about how to use the Divider screen. The best part of the screen for this is the
|
||||
:class:`Prompt <simpleline.render.prompt.Prompt>`.
|
||||
The :class:`Prompt <simpleline.render.prompt.Prompt>` class is responsible for guiding the user
|
||||
by giving them a set of possible options to choose from.
|
||||
|
||||
We will set the message of the prompt inside of the :meth:`UIScreen.prompt` method. We also
|
||||
remove the default option to continue, because it functions the same as quitting when there is
|
||||
only one screen in the application.
|
||||
|
||||
.. code-block:: python
|
||||
:dedent: 0
|
||||
|
||||
def prompt(self, args=None):
|
||||
# Change user prompt
|
||||
prompt = super().prompt()
|
||||
|
||||
# Set message to the user prompt. Give a user hint how he/she may control our application.
|
||||
prompt.set_message("Pass numbers to divider in a format: 'num / num'")
|
||||
|
||||
# Remove continue option from the control. There is no need for that
|
||||
# when we have only one screen.
|
||||
prompt.remove_option('c')
|
||||
|
||||
return prompt
|
||||
|
||||
When we are able to present our content to a user, we want to have the possibility to process
|
||||
user input. For this purpose there is the :meth:`UIScreen.input` method. Input from a user is
|
||||
passed to this method, and the screen may process it, discard it or return it for further
|
||||
processing.
|
||||
|
||||
If input processing is not required and the screen should only be used for displaying
|
||||
information to a user, then the :attr:`UIScreen.input_required` property should be set to `False`.
|
||||
However, in this case you need to :meth:`close <UIScreen.close>`, :meth:`redraw <UIScreen.redraw>`
|
||||
or :ref:`push <screen_handling_label>` a new screen manually. This can be done, for example, in the
|
||||
:meth:`UIScreen.show_all` method.
|
||||
|
||||
.. code-block:: python
|
||||
:dedent: 0
|
||||
|
||||
def input(self, args, key):
|
||||
"""Process input from user and catch numbers with '/' symbol."""
|
||||
|
||||
# Test if user passed valid input for divider.
|
||||
# This will basically take number + number and nothing else and only positive numbers.
|
||||
groups = re.match(r'(\d+) *\/ *(\d+)$', key)
|
||||
if groups:
|
||||
num1 = int(groups[1])
|
||||
num2 = int(groups[2])
|
||||
|
||||
# Dividing by zero is not valid so we won't accept this input from the user. New
|
||||
# input is then required from the user.
|
||||
if num2 == 0:
|
||||
return InputState.DISCARDED
|
||||
|
||||
self._message = int(num1 / num2)
|
||||
|
||||
# Because this input is processed we need to show this screen (show the result)
|
||||
# again by returning PROCESSED_AND_REDRAW.
|
||||
# This will call the refresh method so our new result will be processed inside
|
||||
# of the refresh() method.
|
||||
return InputState.PROCESSED_AND_REDRAW
|
||||
else:
|
||||
# Not input for our screen, try other default inputs. This will result in the
|
||||
# same state as DISCARDED when no default option is used.
|
||||
return key
|
||||
|
||||
|
||||
.. py:currentmodule:: simpleline
|
||||
|
||||
Our screen is finished. Next, we need to use it in our application. To run an application the
|
||||
:class:`App` static class must be used.
|
||||
|
||||
This class will initialize an event loop and the scheduler by the :meth:`App.initialize` method.
|
||||
When the application is initialized, we need to pass our screen to the screen stack (you can
|
||||
pass multiple screens but we have only one here). To pass a screen to the screen stack we will use
|
||||
:class:`ScreenHandler <simpleline.render.screen_handler.ScreenHandler>`. For further explanation
|
||||
on screen scheduling, refer to the :ref:`screen_handling_label` section.
|
||||
|
||||
.. code-block:: python
|
||||
:dedent: 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize application (create scheduler and event loop).
|
||||
App.initialize()
|
||||
|
||||
# Create our screen.
|
||||
screen = DividerScreen()
|
||||
|
||||
# Schedule screen to the screen scheduler.
|
||||
# This can be called only after App.initialize().
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
|
||||
# Run the application. You must have some screen scheduled
|
||||
# otherwise it will end in an infinite loop.
|
||||
App.run()
|
||||
|
||||
Well done! You have your first application in Simpleline.
|
||||
|
||||
Further reading
|
||||
---------------
|
||||
|
||||
I would recommend everyone who wants to use Simpleline to look at the
|
||||
`examples <https://github.com/rhinstaller/python-simpleline/tree/master/examples>`_ which is one
|
||||
of the best sources of information. Another place to look is the :ref:`public_api_label`
|
||||
documentation section.
|
||||
@@ -0,0 +1,23 @@
|
||||
.. Simpleline documentation master file, created by
|
||||
sphinx-quickstart on Fri Nov 3 11:00:41 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Simpleline's documentation!
|
||||
======================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Contents:
|
||||
|
||||
introduction
|
||||
guide
|
||||
api
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
Introduction
|
||||
============
|
||||
|
||||
Simpleline is a text UI framework. Originally a part of the Anaconda installer project.
|
||||
|
||||
It is designed to be used with line-based machines and tools (e.g. serial console) so that
|
||||
every new line it appended to the bottom of the screen. Printed lines are never rewritten!
|
||||
|
||||
How to use
|
||||
----------
|
||||
|
||||
The best learning sources can be found in the
|
||||
`examples directory <https://github.com/rhinstaller/python-simpleline/tree/master/examples>`_ in
|
||||
the `GitHub repository <https://github.com/rhinstaller/python-simpleline>`_ and you can read the
|
||||
:ref:`guide_label` section of this documentation. However, some basic usage of Simpleline will be
|
||||
shown here too, to get an idea of how Simpleline works::
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget
|
||||
|
||||
|
||||
# UIScreen is the main building item for Simpleline. Every screen
|
||||
# which will user see should be inherited from UIScreen.
|
||||
class HelloWorld(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
# Set title of the screen.
|
||||
super().__init__(title=u"Hello World")
|
||||
|
||||
def refresh(self, args=None):
|
||||
# Fill the self.window attribute by the WindowContainer and set screen title as header.
|
||||
super().refresh()
|
||||
widget = TextWidget("Body text")
|
||||
self.window.add_with_separator(widget)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize application (create scheduler and event loop).
|
||||
App.initialize()
|
||||
|
||||
# Create our screen.
|
||||
screen = HelloWorld()
|
||||
|
||||
# Schedule screen to the screen scheduler.
|
||||
# This can be called only after App.initialize().
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
|
||||
# Run the application. You must have some screen scheduled
|
||||
# otherwise it will end in an infinite loop.
|
||||
App.run()
|
||||
|
||||
The output from the simple *Hello World* example above::
|
||||
|
||||
$ ./run_example.sh 00_basic
|
||||
================================================================================
|
||||
================================================================================
|
||||
Hello World
|
||||
|
||||
Body text
|
||||
|
||||
Please make a selection from the above ['c' to continue, 'q' to quit, 'r' to
|
||||
refresh]:
|
||||
|
||||
If a user presses **r** and then **enter** to refresh, the same screen is printed again.
|
||||
This will be printed to a monitor::
|
||||
|
||||
$ ./run_example.sh 00_basic
|
||||
================================================================================
|
||||
================================================================================
|
||||
Hello World
|
||||
|
||||
Body text
|
||||
|
||||
Please make a selection from the above ['c' to continue, 'q' to quit, 'r' to
|
||||
refresh]: r
|
||||
================================================================================
|
||||
================================================================================
|
||||
Hello World
|
||||
|
||||
Body text
|
||||
|
||||
Please make a selection from the above ['c' to continue, 'q' to quit, 'r' to
|
||||
refresh]:
|
||||
|
||||
As you can see the whole screen is not rewritten -- only printed again on the bottom. This
|
||||
is the expected behavior so the actual screen is always at the bottom but you can see the whole
|
||||
history. This behavior makes working with line based machines and tools much easier.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
This is a Python3-only project. This code should not be difficult to migrate to Python2. However,
|
||||
there is no need from the community, so it is only compatible with Python3 at the moment. No special
|
||||
libraries are required to use this library. If you want to use glib event loop instead of the
|
||||
original one you need to install glib and Python3 gobject introspection.
|
||||
|
||||
If you want to run tests (make ci), you need to install
|
||||
`Pocketlint <https://github.com/rhinstaller/pocketlint>`_ and
|
||||
`glib <https://developer.gnome.org/glib/>`_ with gobject introspection for
|
||||
`Python3 <https://docs.python.org/3/index.html>`_.
|
||||
@@ -0,0 +1,36 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=.
|
||||
set BUILDDIR=_build
|
||||
set SPHINXPROJ=Simpleline
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Basic usage of Simpleline.
|
||||
#
|
||||
# Only show screen with header and text in it.
|
||||
#
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget
|
||||
|
||||
|
||||
# UIScreen is the main building item for Simpleline. Every screen
|
||||
# which will user see should be inherited from UIScreen.
|
||||
class HelloWorld(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
# Set title of the screen.
|
||||
super().__init__(title=u"Hello World")
|
||||
|
||||
def refresh(self, args=None):
|
||||
# Fill the self.window attribute by the WindowContainer and set screen title as header.
|
||||
super().refresh()
|
||||
widget = TextWidget("Body text")
|
||||
self.window.add_with_separator(widget)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize application (create scheduler and event loop).
|
||||
App.initialize()
|
||||
|
||||
# Create our screen.
|
||||
screen = HelloWorld()
|
||||
|
||||
# Schedule screen to the screen scheduler.
|
||||
# This can be called only after App.initialize().
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
|
||||
# Run the application. You must have some screen scheduled
|
||||
# otherwise it will end in an infinite loop.
|
||||
App.run()
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Usage of base widgets.
|
||||
#
|
||||
# Show base widgets for user interaction in one screen.
|
||||
#
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget, CenterWidget, CheckboxWidget
|
||||
|
||||
|
||||
class HelloWorld(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
# Set title of the screen.
|
||||
super().__init__(title=u"Show Widgets")
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh()
|
||||
|
||||
# Text widget
|
||||
# Show text to user. This is basic widget which will handle
|
||||
# wrapping of words for you.
|
||||
text_widget = TextWidget("Text widget")
|
||||
self.window.add_with_separator(text_widget)
|
||||
|
||||
# Center widget
|
||||
# Wrap extisting widget and center it to the middle of the screen.
|
||||
text = TextWidget("Center widget")
|
||||
center_widget = CenterWidget(text)
|
||||
self.window.add_with_separator(center_widget, blank_lines=3) # Add two more blank lines
|
||||
|
||||
# Checkbox widget
|
||||
# Checkbox which can hold 2 states.
|
||||
checkbox_widget = CheckboxWidget(key="o",
|
||||
title="Checkbox title",
|
||||
text="Checkbox text",
|
||||
completed=True)
|
||||
self.window.add_with_separator(checkbox_widget)
|
||||
|
||||
# Checkbox widget unchecked
|
||||
checkbox_widget_unchecked = CheckboxWidget(key="o",
|
||||
title="Checkbox title",
|
||||
text="Unchecked",
|
||||
completed=False)
|
||||
self.window.add_with_separator(checkbox_widget_unchecked)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App.initialize()
|
||||
|
||||
screen = HelloWorld()
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
|
||||
App.run()
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# How to block the input from a user.
|
||||
#
|
||||
# This can be used for example to force a user to set all the required values.
|
||||
# Application quit callback is also used here.
|
||||
#
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.prompt import Prompt
|
||||
from simpleline.render.screen import UIScreen, InputState
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget, CenterWidget
|
||||
|
||||
|
||||
def application_quit_cb(args):
|
||||
"""Call this callback when the application is quitting."""
|
||||
print("Application is closing. Bye!")
|
||||
|
||||
|
||||
class InfiniteScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
# We are using title as message here. Any text could be passed to the title.
|
||||
super().__init__("You need to use 'q' to quit")
|
||||
self.continue_count = 0
|
||||
|
||||
def refresh(self, args=None):
|
||||
"""Print text to user with number of continue clicked."""
|
||||
super().refresh(args)
|
||||
# Print counter to the screen.
|
||||
widget = TextWidget("You pressed {} times on continue".format(self.continue_count))
|
||||
# Center this counter to middle of the screen.
|
||||
center_widget = CenterWidget(widget)
|
||||
# Add the centered widget to the window container.
|
||||
self.window.add(center_widget)
|
||||
|
||||
def input(self, args, key):
|
||||
"""Catch 'c' keys for continue and increase counter."""
|
||||
if key == Prompt.CONTINUE:
|
||||
self.continue_count += 1
|
||||
# Do not process 'c' continue anymore.
|
||||
# This will refresh screen to refresh counter number
|
||||
return InputState.PROCESSED_AND_REDRAW
|
||||
|
||||
# Process other input e.g.: 'r' refresh and 'q' quit.
|
||||
return key
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App.initialize()
|
||||
screen = InfiniteScreen()
|
||||
|
||||
# Get event loop from application.
|
||||
loop = App.get_event_loop()
|
||||
# Set quit callback to the loop. When the loop quits this callback will be triggered.
|
||||
loop.set_quit_callback(application_quit_cb)
|
||||
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
App.run()
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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 import App
|
||||
from simpleline.render.screen import UIScreen, InputState
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget, CenterWidget
|
||||
|
||||
|
||||
class Hub(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("Hub for entry counter")
|
||||
self._counter_spoke = CounterScreen()
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
|
||||
w = CenterWidget(TextWidget("Press '1' to enter Entry counter"))
|
||||
|
||||
self.window.add_with_separator(w)
|
||||
|
||||
def input(self, args, key):
|
||||
"""Run spokes based on the user choice."""
|
||||
if key == "1":
|
||||
ScreenHandler.push_screen(self._counter_spoke)
|
||||
# this input was processed
|
||||
return InputState.PROCESSED
|
||||
|
||||
# return for outer processing
|
||||
# the basic processing is 'c' for continue, 'r' for refresh, 'q' to quit
|
||||
# otherwise the input is discarded and waiting for a new input
|
||||
return key
|
||||
|
||||
def prompt(self, args=None):
|
||||
"""Add our information to the prompt."""
|
||||
prompt = super().prompt(args)
|
||||
prompt.add_option("1", "to enter counter spoke")
|
||||
return prompt
|
||||
|
||||
|
||||
class CounterScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("Counter Screen")
|
||||
self._counter = 0
|
||||
|
||||
def closed(self):
|
||||
super().closed()
|
||||
self.screen_ready = False
|
||||
|
||||
def setup(self, args=None):
|
||||
super().setup(args)
|
||||
self._counter += 1
|
||||
return True
|
||||
|
||||
def refresh(self, args=None):
|
||||
"""Write message to user."""
|
||||
super().refresh(args)
|
||||
w = TextWidget("Counter {}".format(self._counter))
|
||||
self.window.add_with_separator(CenterWidget(w))
|
||||
|
||||
@property
|
||||
def counter(self):
|
||||
return self._counter
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App.initialize()
|
||||
hub = Hub()
|
||||
ScreenHandler.schedule_screen(hub)
|
||||
App.run()
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Show help screen.
|
||||
#
|
||||
# Usage of the HelpScreen advanced widget.
|
||||
# There are many of advanced widgets which have different uses.
|
||||
# I've recommend developer to look on them.
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.adv_widgets import HelpScreen
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App.initialize()
|
||||
# You need to pass file with help text to the screen.
|
||||
s = HelpScreen("./04_help/example_help.txt")
|
||||
ScreenHandler.schedule_screen(s)
|
||||
App.run()
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
This is help to our AWESOME application.
|
||||
|
||||
Everything is so simple, therefore no help is needed!
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
# Hub and spoke implementation.
|
||||
#
|
||||
|
||||
# Advanced example of Simpleline use.
|
||||
# Hub is the main screen from where you can go to spokes and do work in the spokes
|
||||
# then you will return to the hub back. You can continue when all the required items
|
||||
# is set.
|
||||
# The example of containers use will be showed here.
|
||||
#
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.adv_widgets import PasswordDialog
|
||||
from simpleline.render.containers import ListRowContainer
|
||||
from simpleline.render.prompt import Prompt
|
||||
from simpleline.render.screen import UIScreen, InputState
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget, CenterWidget
|
||||
|
||||
|
||||
class Hub(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("Hub")
|
||||
|
||||
# Container will be used for spokes positioning. Container is always created in
|
||||
# the refresh() method.
|
||||
self._container = None
|
||||
|
||||
self._create_spokes()
|
||||
|
||||
def _create_spokes(self):
|
||||
"""Create spokes and use their value."""
|
||||
|
||||
# Create name spoke
|
||||
self._name_spoke = SetNameScreen("First name", "John")
|
||||
# Create surname spoke
|
||||
self._surname_spoke = SetNameScreen("Surname", "Doe")
|
||||
# Create the PasswordDialog advanced widget for getting password from a user.
|
||||
self._password_spoke = PasswordDialog()
|
||||
|
||||
def refresh(self, args=None):
|
||||
"""Refresh method is called always before the screen will be printed.
|
||||
|
||||
All items for printing should be updated or created here.
|
||||
"""
|
||||
# Init window container. The windows container will be erased here.
|
||||
# The window container is the base container. Everything for rendering should be put
|
||||
# into this container, including other containers.
|
||||
super().refresh(args)
|
||||
|
||||
# Add the screen header message before our items.
|
||||
header = TextWidget("Please complete all the spokes to continue")
|
||||
header = CenterWidget(header)
|
||||
self.window.add_with_separator(header, blank_lines=2)
|
||||
|
||||
# Create the empty container.
|
||||
# It will add numbering, process user input and positioning for us.
|
||||
self._container = ListRowContainer(2)
|
||||
|
||||
# Create widget to get user name.
|
||||
widget = self._create_name_widget()
|
||||
# Add widget, callback, arguments to the container.
|
||||
#
|
||||
# widget - Widget we want to render. It will be numbered automatically.
|
||||
# Could be container if needed.
|
||||
# callback - This callback will be called by the ListRowContainer.process_user_input()
|
||||
# method when a user press the number of this item. Callback will get args
|
||||
# passed as 3rd argument.
|
||||
# args - Argument for callback.
|
||||
self._container.add(widget, self._push_screen_callback, self._name_spoke)
|
||||
|
||||
# Create surname widget and add it to the container.
|
||||
widget = self._create_surname_widget()
|
||||
self._container.add(widget, self._push_screen_callback, self._surname_spoke)
|
||||
|
||||
# Create password widget and add it to the container.
|
||||
widget = self._create_password_widget()
|
||||
self._container.add(widget, self._push_screen_callback, self._password_spoke)
|
||||
|
||||
# Add the ListRowContainer container to the WindowContainer container.
|
||||
self.window.add_with_separator(self._container)
|
||||
|
||||
def _create_name_widget(self):
|
||||
"""Create name spoke widget.
|
||||
|
||||
Add the actual value below the spoke name.
|
||||
"""
|
||||
msg = "First name"
|
||||
if self._name_spoke.value:
|
||||
msg += "\n{}".format(self._name_spoke.value)
|
||||
|
||||
return TextWidget(msg)
|
||||
|
||||
def _create_surname_widget(self):
|
||||
"""Create surname spoke widget.
|
||||
|
||||
Add the actual value below the spoke name.
|
||||
"""
|
||||
msg = "Surname"
|
||||
if self._surname_spoke.value:
|
||||
msg += "\n{}".format(self._surname_spoke.value)
|
||||
|
||||
return TextWidget(msg)
|
||||
|
||||
def _create_password_widget(self):
|
||||
"""Create password spoke widget.
|
||||
|
||||
Add the "Password set" text below the spoke name if set.
|
||||
"""
|
||||
msg = "Password"
|
||||
if self._password_spoke.answer:
|
||||
msg += "\nPassword set."
|
||||
|
||||
return TextWidget(msg)
|
||||
|
||||
def input(self, args, key):
|
||||
"""Run spokes based on the user choice."""
|
||||
# Find out if a user pressed number for an existing widget and call the callback attached
|
||||
# to it with arguments passed in the refresh() method.
|
||||
# Return False if the input is not related to the widget.
|
||||
if self._container.process_user_input(key):
|
||||
# Do not process other input if spoke is entered.
|
||||
return InputState.PROCESSED
|
||||
|
||||
# Block continue ('c') if everything is not set.
|
||||
if key == Prompt.CONTINUE:
|
||||
if self._name_spoke and self._surname_spoke and self._password_spoke.answer:
|
||||
return key
|
||||
# catch 'c' key if not everything set
|
||||
return InputState.DISCARDED
|
||||
|
||||
return key
|
||||
|
||||
@staticmethod
|
||||
def _push_screen_callback(target_screen):
|
||||
"""Push target screen as new screen.
|
||||
|
||||
Target screen is passed in as an argument in the refresh() method.
|
||||
"""
|
||||
ScreenHandler.push_screen(target_screen)
|
||||
|
||||
def prompt(self, args=None):
|
||||
"""Add information to prompt for user."""
|
||||
prompt = super().prompt(args)
|
||||
# Give user hint that he can press 1, 2 or 3 to enter spokes.
|
||||
prompt.add_option("1,2,3", "to enter spokes")
|
||||
return prompt
|
||||
|
||||
|
||||
class SetNameScreen(UIScreen):
|
||||
|
||||
def __init__(self, message, def_value):
|
||||
"""Create spoke for setting name and surname.
|
||||
|
||||
:param message: Text message as the body for user.
|
||||
:param def_value: Default value for this spoke.
|
||||
"""
|
||||
super().__init__()
|
||||
self._value = def_value
|
||||
self._message = message
|
||||
|
||||
def refresh(self, args=None):
|
||||
"""Write message to user."""
|
||||
super().refresh(args)
|
||||
w = TextWidget(self._message)
|
||||
self.window.add(CenterWidget(w))
|
||||
|
||||
def prompt(self, args=None):
|
||||
"""Take user input."""
|
||||
self._value = self.get_user_input("Write your name: ")
|
||||
self.close()
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Return value set by a user in this spoke."""
|
||||
return self._value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App.initialize()
|
||||
screen = Hub()
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
App.run()
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Use glib event loop instead of the original Simpleline loop.
|
||||
#
|
||||
# You need to have the python3-gobject installed.
|
||||
#
|
||||
# You can install it on Fedora by running:
|
||||
#
|
||||
# dnf install python3-gobject-base
|
||||
#
|
||||
#
|
||||
# This is basic example using Glib event loop.
|
||||
# You can implement your own loop abstraction. See simpleline/event_loop/glib_event_loop.
|
||||
#
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.event_loop.glib_event_loop import GLibEventLoop
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget
|
||||
|
||||
|
||||
class HelloWorld(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(title=u"Hello World with GLib")
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh()
|
||||
self.window.add_with_separator(TextWidget("Body text"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Create Glib event loop.
|
||||
glib_loop = GLibEventLoop()
|
||||
# Use glib event loop instead of the original one.
|
||||
# Everything else should behave the same as with the original Simpleline loop.
|
||||
App.initialize(event_loop=glib_loop)
|
||||
|
||||
screen = HelloWorld()
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
|
||||
# Run the application.
|
||||
App.run()
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/bin/python3
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Simple divider screen.
|
||||
#
|
||||
# User input processing example.
|
||||
#
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.screen import UIScreen, InputState
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
from simpleline.render.widgets import TextWidget
|
||||
|
||||
|
||||
class DividerScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
# Set title of the screen.
|
||||
super().__init__(title=u"Divider")
|
||||
self._message = 0
|
||||
|
||||
def refresh(self, args=None):
|
||||
# Fill the self.window attribute by the WindowContainer and set screen title as header.
|
||||
super().refresh()
|
||||
|
||||
widget = TextWidget("Result: " + str(self._message))
|
||||
self.window.add_with_separator(widget)
|
||||
|
||||
def prompt(self, args=None):
|
||||
# Change user prompt
|
||||
prompt = super().prompt()
|
||||
|
||||
# Set message to the user prompt. Give a user hint how he/she may control our application.
|
||||
prompt.set_message("Pass numbers to divider in a format: 'num / num'")
|
||||
|
||||
# Remove continue option from the control. There is no need for that
|
||||
# when we have only one screen.
|
||||
prompt.remove_option('c')
|
||||
|
||||
return prompt
|
||||
|
||||
def input(self, args, key):
|
||||
"""Process input from user and catch numbers with '/' symbol."""
|
||||
|
||||
# Test if user passed valid input for divider.
|
||||
# This will basically take number + number and nothing else and only positive numbers.
|
||||
groups = re.match(r'(\d+) *\/ *(\d+)$', key)
|
||||
if groups:
|
||||
num1 = int(groups[1])
|
||||
num2 = int(groups[2])
|
||||
|
||||
# Dividing by zero is not valid so we won't accept this input from the user. New
|
||||
# input is then required from the user.
|
||||
if num2 == 0:
|
||||
return InputState.DISCARDED
|
||||
|
||||
self._message = int(num1 / num2)
|
||||
|
||||
# Because this input is processed we need to show this screen (show the result).
|
||||
# This will call refresh so our new result will be processed inside of the refresh()
|
||||
# method.
|
||||
return InputState.PROCESSED_AND_REDRAW
|
||||
|
||||
# Not input for our screen, try other default inputs. This will result in the
|
||||
# same state as DISCARDED when no default option is used.
|
||||
return key
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize application (create scheduler and event loop).
|
||||
App.initialize()
|
||||
|
||||
# Create our screen.
|
||||
screen = DividerScreen()
|
||||
|
||||
# Schedule screen to the screen scheduler.
|
||||
# This can be called only after App.initialize().
|
||||
ScreenHandler.schedule_screen(screen)
|
||||
|
||||
# Run the application. You must have some screen scheduled
|
||||
# otherwise it will end in an infinite loop.
|
||||
App.run()
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Script for starting examples from source code.
|
||||
#
|
||||
# Author(s): Jiri Konecny <jkonecny@redhat.com>
|
||||
#
|
||||
|
||||
|
||||
function print_help {
|
||||
echo "run_example.sh - easy way how to run example without installing module"
|
||||
echo ""
|
||||
echo "./run_example.sh [example]"
|
||||
echo ""
|
||||
echo "There is one required argument [example] which is name of the test."
|
||||
echo ""
|
||||
}
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Bad number of arguments" 1>&2
|
||||
print_help
|
||||
exit 1
|
||||
elif [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]]; then
|
||||
print_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PROJECT_NAME=${1%%/}
|
||||
|
||||
pushd $(pwd)
|
||||
cd $(dirname $0)/
|
||||
PYTHONPATH="..:." python3 ./$PROJECT_NAME/$PROJECT_NAME.py
|
||||
popd
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# Makefile for the PO files (translation) catalog
|
||||
#
|
||||
# $Id$
|
||||
|
||||
TOP = ../..
|
||||
|
||||
# What is this package?
|
||||
NLSPACKAGE = python-simpleline
|
||||
POTFILE = $(NLSPACKAGE).pot
|
||||
INSTALL = /usr/bin/install -c
|
||||
INSTALL_DATA = $(INSTALL) -m 644
|
||||
INSTALL_DIR = /usr/bin/install -d
|
||||
|
||||
# destination directory
|
||||
INSTALL_NLS_DIR = $(RPM_BUILD_ROOT)/usr/share/locale
|
||||
|
||||
# PO catalog handling
|
||||
MSGMERGE = msgmerge -v
|
||||
XGETTEXT = xgettext --default-domain=$(NLSPACKAGE) \
|
||||
--add-comments
|
||||
MSGFMT = msgfmt --statistics --verbose
|
||||
|
||||
# What do we need to do
|
||||
POFILES = $(wildcard *.po)
|
||||
MOFILES = $(patsubst %.po,%.mo,$(POFILES))
|
||||
PYSRC = $(wildcard ../simpleline/*.py) $(wildcard ../simpleline/**/*.py)
|
||||
|
||||
all:: update-po $(MOFILES)
|
||||
|
||||
potfile: $(PYSRC)
|
||||
$(XGETTEXT) -L Python --keyword=_ --keyword=N_ $(PYSRC)
|
||||
@if cmp -s $(NLSPACKAGE).po $(POTFILE); then \
|
||||
rm -f $(NLSPACKAGE).po; \
|
||||
else \
|
||||
mv -f $(NLSPACKAGE).po $(POTFILE); \
|
||||
fi; \
|
||||
|
||||
update-po: Makefile potfile refresh-po
|
||||
|
||||
refresh-po: Makefile
|
||||
for cat in $(POFILES); do \
|
||||
lang=`basename $$cat .po`; \
|
||||
if $(MSGMERGE) $$lang.po $(POTFILE) > $$lang.pot ; then \
|
||||
mv -f $$lang.pot $$lang.po ; \
|
||||
echo "$(MSGMERGE) of $$lang succeeded" ; \
|
||||
else \
|
||||
echo "$(MSGMERGE) of $$lang failed" ; \
|
||||
rm -f $$lang.pot ; \
|
||||
fi \
|
||||
done
|
||||
|
||||
clean:
|
||||
@rm -fv *mo *~ .depend
|
||||
|
||||
install: $(MOFILES)
|
||||
@for n in $(MOFILES); do \
|
||||
l=`basename $$n .mo`; \
|
||||
$(INSTALL_DIR) $(INSTALL_NLS_DIR)/$$l/LC_MESSAGES; \
|
||||
$(INSTALL_DATA) --verbose $$n $(INSTALL_NLS_DIR)/$$l/LC_MESSAGES/$(NLSPACKAGE).mo; \
|
||||
done
|
||||
|
||||
%.mo: %.po
|
||||
$(MSGFMT) -o $@ $<
|
||||
|
||||
.PHONY: missing depend
|
||||
|
||||
|
||||
+408
@@ -0,0 +1,408 @@
|
||||
%global srcname simpleline
|
||||
|
||||
Name: python-%{srcname}
|
||||
Summary: A Python library for creating text UI
|
||||
Url: https://github.com/rhinstaller/python-%{srcname}
|
||||
Version: 1.8
|
||||
Release: 1%{?dist}
|
||||
# This tarball was created from upstream git:
|
||||
# git clone https://github.com/rhinstaller/python-simpleline
|
||||
# cd python-simpleline && make archive
|
||||
Source0: https://github.com/rhinstaller/python-%{srcname}/releases/download/%{srcname}-%{version}/%{srcname}-%{version}.tar.gz
|
||||
|
||||
License: LGPLv3+
|
||||
BuildArch: noarch
|
||||
BuildRequires: make
|
||||
BuildRequires: python3-devel
|
||||
BuildRequires: gettext
|
||||
BuildRequires: python3-setuptools
|
||||
BuildRequires: intltool
|
||||
BuildRequires: python3-gobject-base
|
||||
|
||||
%description
|
||||
Simpleline is a Python library for creating text UI.
|
||||
It is designed to be used with line-based machines
|
||||
and tools (e.g. serial console) so that every new line
|
||||
is appended to the bottom of the screen.
|
||||
Printed lines are never rewritten!
|
||||
|
||||
|
||||
%package -n python3-%{srcname}
|
||||
Summary: A Python3 library for creating text UI
|
||||
Requires: rpm-python3
|
||||
|
||||
%{?python_provide:%python_provide python3-%{srcname}}
|
||||
|
||||
%description -n python3-%{srcname}
|
||||
Simpleline is a Python3 library for creating text UI.
|
||||
It is designed to be used with line-based machines
|
||||
and tools (e.g. serial console) so that every new line
|
||||
is appended to the bottom of the screen.
|
||||
Printed lines are never rewritten!
|
||||
|
||||
%prep
|
||||
%setup -q -n %{srcname}-%{version}
|
||||
|
||||
%build
|
||||
%make_build
|
||||
|
||||
%install
|
||||
make DESTDIR=%{buildroot} install
|
||||
%find_lang python-%{srcname}
|
||||
|
||||
%check
|
||||
make test
|
||||
|
||||
|
||||
%files -n python3-%{srcname} -f python-%{srcname}.lang
|
||||
%license LICENSE.md
|
||||
%doc ChangeLog README.md
|
||||
%{python3_sitelib}/*
|
||||
|
||||
%changelog
|
||||
* Mon Feb 22 2021 Jiri Konecny <jkonecny@redhat.com> - 1.8-1
|
||||
- Add missing make command to spec file (jkonecny)
|
||||
- Don't translate the prompt keys (vponcova)
|
||||
- Enable daily build for Anaconda-devel COPR repository (jkonecny)
|
||||
- Use Fedora container registry instead of Dockerhub (jkonecny)
|
||||
- Migrate COPR daily COPR builds to Packit (jkonecny)
|
||||
- Test build on Fedora ELN (jkonecny)
|
||||
- Remove packit get-current-action (jkonecny)
|
||||
- Run tests in GitHub workflow (martin)
|
||||
- Fix raise-missing-from (W0707) pylint warnings (martin)
|
||||
- Fix pylint to check test code correctly (jkonecny)
|
||||
- Use script to run unit tests (jkonecny)
|
||||
- Use relative imports in tests (jkonecny)
|
||||
- Change directory structure of unit tests (jkonecny)
|
||||
- Fix documentation of _process_screen method (jkonecny)
|
||||
- Fix pylint issues (jkonecny)
|
||||
- Use pylint instead of pocketlint (jkonecny)
|
||||
- Add coverage support (jkonecny)
|
||||
- Make link to exmples directory in Readme (jkonecny)
|
||||
- Fix homepage of the project in setup.py (jkonecny)
|
||||
- Fix classifiers in setup.py (jkonecny)
|
||||
- Add pypi-upload to Makefile (jkonecny)
|
||||
- Use correct variant of the field (jkonecny)
|
||||
- Propose Fedora update only to Fedora in development (jkonecny)
|
||||
- Add upstream tag template to packit for releasing (jkonecny)
|
||||
- Packit will download archive from Source0 if needed (jkonecny)
|
||||
|
||||
* Thu Jun 25 2020 Jiri Konecny <jkonecny@redhat.com> - 1.7-1
|
||||
- Testing farm seems like a better option (jkonecny)
|
||||
- Fix upstream package name (jkonecny)
|
||||
- Copy archive to the project root for packit (jkonecny)
|
||||
- Use BUILD_ARGS for packit to remove wheel dependency (jkonecny)
|
||||
- Correct how packit sniffing package version (jkonecny)
|
||||
- Add packit action to create archive correctly (jkonecny)
|
||||
- Simplify Packit configuration file (jkonecny)
|
||||
- Enable copr builds and add packit config (dhodovsk)
|
||||
- Add possibility to change setup.py arguments (jkonecny)
|
||||
- Fix typo in setup.py (jkonecny)
|
||||
- Add translation badge from Weblate (jkonecny)
|
||||
- Add potfile to the .gitignore (jkonecny)
|
||||
- Pause releasing when calling bumpver (jkonecny)
|
||||
- Remove unused PREFIX from the Makefile (jkonecny)
|
||||
- Remove Zanata from the Simpleline (jkonecny)
|
||||
- Switch po-push from Zanata to Weblate (jkonecny)
|
||||
- Download translations from Weblate (jkonecny)
|
||||
- Remove pot file from this repository (jkonecny)
|
||||
- Add new directories to .gitignore and make clean (jkonecny)
|
||||
- Add Makefile PHONY targets (jkonecny)
|
||||
- Simplify archive creation (jkonecny)
|
||||
- Reflect rename of COPYING to LICENSE.md (jkonecny)
|
||||
- Change license in spec file to LGPLv3+ (jkonecny)
|
||||
- Add COPR badge for daily builds (jkonecny)
|
||||
- Improve setup.py (jkonecny)
|
||||
- Change license to LGPLv3 for all the files (jkonecny)
|
||||
- Change License from GPLv2+ to LGPLv3+ (jkonecny)
|
||||
|
||||
* Thu Aug 22 2019 Jiri Konecny <jkonecny@redhat.com> - 1.6-1
|
||||
- Always close the password dialog (vponcova)
|
||||
- Remove unnecessary pass statements (jkonecny)
|
||||
|
||||
* Mon May 13 2019 Jiri Konecny <jkonecny@redhat.com> - 1.5-1
|
||||
- Update spec file from the downstream (jkonecny)
|
||||
- Fix translation issue for lt language (jkonecny)
|
||||
|
||||
* Sat Feb 02 2019 Fedora Release Engineering <releng@fedoraproject.org> - 1.4-2
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild
|
||||
|
||||
* Mon Nov 19 2018 Jiri Konecny <jkonecny@redhat.com> - 1.4-1
|
||||
- Fix and add tests for the new changes (#1646568) (jkonecny)
|
||||
- Add should_run_with_empty_stack configuration (#1646568) (jkonecny)
|
||||
- Remove wrong line in password_function conf (jkonecny)
|
||||
- Tweak date lang settings in make bumpver command (jkonecny)
|
||||
- Drop python-pocketlint build dependency (jkonecny)
|
||||
- Update spec file from Fedora (jkonecny)
|
||||
|
||||
* Sat Jul 14 2018 Fedora Release Engineering <releng@fedoraproject.org> - 1.3-5
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild
|
||||
|
||||
* Mon Jul 02 2018 Miro Hrončok <mhroncok@redhat.com> - 1.3-4
|
||||
- Rebuilt for Python 3.7
|
||||
|
||||
* Wed Jun 20 2018 Jiri Konecny <jkonecny@redhat.com> - 1.3-3
|
||||
- Drop python-pocketlint dependency
|
||||
|
||||
* Mon Jun 18 2018 Miro Hrončok <mhroncok@redhat.com> - 1.3-2
|
||||
- Rebuilt for Python 3.7
|
||||
|
||||
* Mon Jun 11 2018 Jiri Konecny <jkonecny@redhat.com> - 1.3-1
|
||||
- Fix pylint errors raised by new pylint version (jkonecny)
|
||||
- Setup logging handler properly for a library (jkonecny)
|
||||
|
||||
* Tue May 29 2018 Jiri Konecny <jkonecny@redhat.com> - 1.2-1
|
||||
- Document GlobalConfiguration class (jkonecny)
|
||||
- Add advanced input processing section to docs (jkonecny)
|
||||
- Make makefile zanata check more robust (jkonecny)
|
||||
- Add missing newline characters at the end (jkonecny)
|
||||
- Abstract widget buffer extensions (jkonecny)
|
||||
- Use python 3 sphinx module (jkonecny)
|
||||
|
||||
* Tue Apr 10 2018 Jiri Konecny <jkonecny@redhat.com> - 1.1-1
|
||||
- Add global configuration initialize test (jkonecny)
|
||||
- Use default password function from GlobalConfiguration (jkonecny)
|
||||
- Add tests for GlobalConfiguration class (jkonecny)
|
||||
- Move default width to GlobalConfiguration class (jkonecny)
|
||||
- Add GlobalConfiguration object (jkonecny)
|
||||
- Fix a missing article in docs. (jkonecny)
|
||||
- Add tests for UIScreen get_user_input method (jkonecny)
|
||||
- Only InputManager is used to get input in UIScreen (jkonecny)
|
||||
- Move emit of InputReadySignal to InputRequest (jkonecny)
|
||||
- Get only last input on concurrent input (jkonecny)
|
||||
- Add concurrency tests for InputHandler (jkonecny)
|
||||
- Add requester source to the InputHandler (#1557472) (jkonecny)
|
||||
- Use InputManager as screen helper not in ScreenScheduler (#1557472) (jkonecny)
|
||||
- Add test to check InputThreadManager after initialization (jkonecny)
|
||||
- Reinitialize InputThreadManager when app is initialized (jkonecny)
|
||||
- Input requests are now handled by InputThreadManager (jkonecny)
|
||||
- Remove io_manager property from ScreenScheduler (jkonecny)
|
||||
- Rename io_manager to input_manager (jkonecny)
|
||||
- Change IOManager to InputManager (jkonecny)
|
||||
- Move draw from IOManager to ScreenScheduler (jkonecny)
|
||||
- Separate password input to PasswordInputHandler (jkonecny)
|
||||
- Add DEFAULT_WIDTH constant (jkonecny)
|
||||
- Add tests for InputHandler (jkonecny)
|
||||
- Replace get_input_without_check by new property (jkonecny)
|
||||
- Separate input to a new module (jkonecny)
|
||||
|
||||
* Wed Feb 28 2018 Jiri Konecny <jkonecny@redhat.com> - 1.0-1
|
||||
- Fix docs based on the new PROCESSED feature (jkonecny)
|
||||
- Use new PROCESS_AND* in examples (jkonecny)
|
||||
- Add tests for PROCESSED_AND_{CLOSE|REDRAW} (jkonecny)
|
||||
- Use new PROCESSED_AND_{CLOSE|REDRAW} in advanced widgets (jkonecny)
|
||||
- Support PROCESSED_AND_CLOSE UserInputState (jkonecny)
|
||||
- Rename UserInputResult to UserInputAction (jkonecny)
|
||||
- Support PROCESSED_AND_REDRAW (jkonecny)
|
||||
- Fix link to the documentation in README (jkonecny)
|
||||
- Add link to the documentation (jkonecny)
|
||||
- Write documentation for Simpleline (jkonecny)
|
||||
- Disable pylint error for doc copyright variable (jkonecny)
|
||||
- Add new example used in the documentation (jkonecny)
|
||||
- Mock modules for readthedocs (jkonecny)
|
||||
- Do a small fixes to improve documentation (jkonecny)
|
||||
- Get version from the spec file (jkonecny)
|
||||
- Add documentation skeleton (jkonecny)
|
||||
|
||||
* Thu Oct 19 2017 Jiri Konecny <jkonecny@redhat.com> - 0.8-1
|
||||
- Test clean-up (jkonecny)
|
||||
- Move and rename force_quit loop (jkonecny)
|
||||
- Add dump screen stack method (jkonecny)
|
||||
|
||||
* Tue Oct 17 2017 Jiri Konecny <jkonecny@redhat.com> - 0.7-1
|
||||
- Fix crash when container callback is not set (jkonecny)
|
||||
- Add EntryWidget to show item title and value (jkonecny)
|
||||
- Add GetPasswordInputScreen for getting passwords (jkonecny)
|
||||
- UIScreen can have hidden input (jkonecny)
|
||||
- Move getpass func inside locks (jkonecny)
|
||||
- Implement GetInputScreen (jkonecny)
|
||||
- Do not print new line with empty container (jkonecny)
|
||||
- Add no_separator to UIScreen (jkonecny)
|
||||
- Fix HelpScreen title (jkonecny)
|
||||
- Remove original classes from GLib tests (jkonecny)
|
||||
- Fix test case name (jkonecny)
|
||||
- Move tests to subfolder (jkonecny)
|
||||
- Move base widgets as second example (jkonecny)
|
||||
- Add example with basic widgets usage (jkonecny)
|
||||
- Improve README.md (jkonecny)
|
||||
- Improve example starting script (jkonecny)
|
||||
- Add glib loop example (jkonecny)
|
||||
- Add prefixes to examples to impress difficulty (jkonecny)
|
||||
- Add comments to the examples (jkonecny)
|
||||
- Update examples to use new features (jkonecny)
|
||||
|
||||
* Fri Sep 08 2017 Jiri Konecny <jkonecny@redhat.com> - 0.6-1
|
||||
- Implement the force quit all loops feature (jkonecny)
|
||||
- Improved GLib event loop testing (jkonecny)
|
||||
- Handle GLib event loop exceptions better (jkonecny)
|
||||
- Support for GLib event loop (jkonecny)
|
||||
- Wrap exceptions from handlers as ExceptionSignal (jkonecny)
|
||||
- Only highest priority events are processed in one iteration (jkonecny)
|
||||
- Move parts from MainLoop to AbstractEventLoop (jkonecny)
|
||||
- MainLoop won't wait in busy loop anymore (jkonecny)
|
||||
- Don't block loop when waiting on user input (jkonecny)
|
||||
- Fix user input lock (jkonecny)
|
||||
|
||||
* Mon Sep 04 2017 Jiri Konecny <jkonecny@redhat.com> - 0.5-1
|
||||
- Fix exception in Screen input caused infinite loop (jkonecny)
|
||||
- Add test if app is initialized (jkonecny)
|
||||
- Redraw on first scheduled screen (#1487326) (jkonecny)
|
||||
- Redraw after YesNoDialog modal window close (jkonecny)
|
||||
|
||||
* Fri Aug 18 2017 Jiri Konecny <jkonecny@redhat.com> - 0.4-1
|
||||
- Screen scheduling is moved to ScreenHandler (jkonecny)
|
||||
- Remove merge commits from changelog (jkonecny)
|
||||
|
||||
* Thu Aug 17 2017 Jiri Konecny <jkonecny@redhat.com> - 0.3-1
|
||||
- Remove merge commits from changelog (jkonecny)
|
||||
- Fix bad input processing for list containers (jkonecny)
|
||||
- Rename quit callback and add args there. (jkonecny)
|
||||
- Add tests for new container feature (jkonecny)
|
||||
- List containers take max width if not specified (jkonecny)
|
||||
- Remove leftover print in containers parsing input (jkonecny)
|
||||
|
||||
* Mon Aug 14 2017 Jiri Konecny <jkonecny@redhat.com> - 0.2-1
|
||||
- Merge pull request #26 from jkonecny12/master-fix-ExitMainLoop-exception (jkonecny)
|
||||
- Merge pull request #28 from jkonecny12/master-fix-container-callback (jkonecny)
|
||||
- Fix docs for container callbacks (jkonecny)
|
||||
- Merge pull request #25 from jkonecny12/master-fix-big-screen-printing (jkonecny)
|
||||
- Remove missed test helper for local testing (jkonecny)
|
||||
- The ExitMainLoop exception should kill whole app (jkonecny)
|
||||
- Do not run an old event queue from modal screen (jkonecny)
|
||||
- Merge pull request #23 from jkonecny12/master-add-possibility-to-end-loop-politely (jkonecny)
|
||||
- Merge pull request #24 from jkonecny12/master-fix-compatibility (jkonecny)
|
||||
- Add possibility to end loop politely (jkonecny)
|
||||
- Merge pull request #22 from jkonecny12/master-add-logging (jkonecny)
|
||||
- Fix printing issues for bigger screens (jkonecny)
|
||||
- Set default screen height to 30 (jkonecny)
|
||||
- Keep backward compatibility for UIScreen args (jkonecny)
|
||||
- Add logging to the Simpleline (jkonecny)
|
||||
- Merge pull request #19 from jkonecny12/master-rework-wait-on-input (jkonecny)
|
||||
- Merge pull request #21 from jkonecny12/master-fix-adv-widgets (jkonecny)
|
||||
- Merge pull request #20 from jkonecny12/master-move-InputState (jkonecny)
|
||||
- Wait on input thread to finish (jkonecny)
|
||||
- return_after don't skip signals in recursion (jkonecny)
|
||||
- Add TicketMachine helper class (jkonecny)
|
||||
- Return user input in the signal handler (jkonecny)
|
||||
- Merge pull request #16 from jkonecny12/master-modify-exception-handling (jkonecny)
|
||||
- Merge pull request #18 from jkonecny12/master-ignore-pylint-with-lock (jkonecny)
|
||||
- Reflect changes in adv_widgets (jkonecny)
|
||||
- Move InputState next to UIScreen (jkonecny)
|
||||
- Break App's cyclic imports (jkonecny)
|
||||
- Add temporal pylint-disable for with Lock (jkonecny)
|
||||
- Re-raise exception only if no handler is registered (jkonecny)
|
||||
- Merge pull request #15 from jkonecny12/master-thread-safe-eventqueue (jkonecny)
|
||||
- Merge pull request #11 from jkonecny12/master-add-args-to-scheduler-shortcut (jkonecny)
|
||||
- Make the EventQueue thread safe (jkonecny)
|
||||
- Merge pull request #13 from jkonecny12/master-enhance-WindowContainer-add (jkonecny)
|
||||
- Merge pull request #12 from jkonecny12/master-fix-ready-collision (jkonecny)
|
||||
- Add args param to the scheduler_handler shortcuts (jkonecny)
|
||||
- Fix bad param name for scheduling methods (jkonecny)
|
||||
- Merge pull request #10 from jkonecny12/master-add-hidden-user-input (jkonecny)
|
||||
- Merge pull request #8 from jkonecny12/master-remove-base-file (jkonecny)
|
||||
- Merge pull request #9 from jkonecny12/master-remove-quit-message (jkonecny)
|
||||
- Add method add_with_separator to WindowContainer (jkonecny)
|
||||
- Fix `ready` property collision with Anaconda (jkonecny)
|
||||
- Add hidden parameter to UIScreen get input (jkonecny)
|
||||
- Remove unused quit message (jkonecny)
|
||||
- Move App from base to __init__.py (jkonecny)
|
||||
- Merge pull request #6 from jkonecny12/fix-make-potfile-generation (jkonecny)
|
||||
- Merge pull request #7 from jkonecny12/master-fix-make-archive (jkonecny)
|
||||
- Fix make archive (jkonecny)
|
||||
- Fix potfile generation (jkonecny)
|
||||
- Merge pull request #5 from jkonecny12/master-add-containers (jkonecny)
|
||||
- Examples now using WindowContainer (jkonecny)
|
||||
- Fix error when getting input directly (jkonecny)
|
||||
- Add WindowContainer and use it in UIScreen (jkonecny)
|
||||
- Add SeparatorWidget (jkonecny)
|
||||
- Add input processing to containers (jkonecny)
|
||||
- Add containers numbering (jkonecny)
|
||||
- Add containers, checkbox and center widget tests (jkonecny)
|
||||
- Add row and column list containers (jkonecny)
|
||||
- Merge pull request #4 from jkonecny12/master-split-renderer (jkonecny)
|
||||
- Fix setup.py modules (jkonecny)
|
||||
- Change INPUT_PROCESSED and INPUT_DISCARDED to Enum (jkonecny)
|
||||
- Rename renderer and event_loop in App class (jkonecny)
|
||||
- Add scheduler_handler and rename switch_screen (jkonecny)
|
||||
- Process signals when waiting on user input (jkonecny)
|
||||
- Move draw screen to the InOutManager (jkonecny)
|
||||
- Change UIScreen properties (jkonecny)
|
||||
- Move input processing to the InOutManager class (jkonecny)
|
||||
- Move SignalHandler to the screen module (jkonecny)
|
||||
- Move ui_screen to the screen module (jkonecny)
|
||||
- Rename Renderer to ScreenScheduler (jkonecny)
|
||||
- Merge pull request #3 from jkonecny12/master-change-close-to-signal (jkonecny)
|
||||
- Change separator tests to reflect close as signal (jkonecny)
|
||||
- Close screen is now signal (jkonecny)
|
||||
- Rename emit_draw_signal to redraw (jkonecny)
|
||||
- Merge pull request #2 from jkonecny12/master-fix-modal (jkonecny)
|
||||
- Add missing doc for EventQueue and ScreeStack (jkonecny)
|
||||
- Add tests for event queue (jkonecny)
|
||||
- Modal screens are now executing new event loop (jkonecny)
|
||||
- Separator should be printed just before show_all() (jkonecny)
|
||||
- Move screen scheduling tests to new file (jkonecny)
|
||||
- Merge pull request #1 from jkonecny12/master-refactorization (jkonecny)
|
||||
- Implement small fixes and tweaks (jkonecny)
|
||||
- Move ScreenStack to render module (jkonecny)
|
||||
- Add SimplelineError as base class for Exceptions (jkonecny)
|
||||
- Clean up unused code and do a small tweaks (jkonecny)
|
||||
- Fix pylint errors (jkonecny)
|
||||
- Modify examples to reflect new changes (jkonecny)
|
||||
- Add emit_draw_signal to the SignalHandler (jkonecny)
|
||||
- Do not call draw when prompt is not present (jkonecny)
|
||||
- Remove old INPUT_* constants (jkonecny)
|
||||
- Do not redraw when input was processed (jkonecny)
|
||||
- Add license to all test classes (jkonecny)
|
||||
- Add new tests for render_screen, renderer, widgets (jkonecny)
|
||||
- Remove hubQ (jkonecny)
|
||||
- Split input from drawing (jkonecny)
|
||||
- Fix rendering errors by removing _redraw variable (jkonecny)
|
||||
- Replace array with priority by Signal ordering (jkonecny)
|
||||
- Add tests for SignalHandler (jkonecny)
|
||||
- Use SignalHandler (jkonecny)
|
||||
- Add screen rendering tests (jkonecny)
|
||||
- Raise exceptions instead of silently quitting (jkonecny)
|
||||
- Reflect code changes in tests (jkonecny)
|
||||
- Fix missing super().__init__() in Signals (jkonecny)
|
||||
- Fix modal screen by removing execute_new_loop (jkonecny)
|
||||
- Fail when UIScreen is not closing itself (jkonecny)
|
||||
- Add constants INPUT_PROCESSED and INPUT_DISCARDED (jkonecny)
|
||||
- Fix issues in Renderer and MainLoop (jkonecny)
|
||||
- Use RenderScreenSignal signal for rendering (jkonecny)
|
||||
- Add App.run as a shortcut to start application (jkonecny)
|
||||
- Move widgets and adv_widgets to render module (jkonecny)
|
||||
- Fix cosmetic bugs in widgets (jkonecny)
|
||||
- Move prompt to render module (jkonecny)
|
||||
- Modify ScreenStack tests to reflect UIScreen changes (jkonecny)
|
||||
- Modify UIScreen to use new App class structure (jkonecny)
|
||||
- Fix renderer tests after moving the Renderer class (jkonecny)
|
||||
- Move renderer to separate file renderer.py (jkonecny)
|
||||
- Add quit_callback to the event loop abstract class (jkonecny)
|
||||
- Add tests for the App class (jkonecny)
|
||||
- Change base class to singleton like class (jkonecny)
|
||||
- Add tests for event loop processing (jkonecny)
|
||||
- Implementation of defualt event loop and signals (jkonecny)
|
||||
- Add abstract base classes for EventLoop and Event (jkonecny)
|
||||
- Remove ScheduleScreen tests (jkonecny)
|
||||
- Add tests for renderer (jkonecny)
|
||||
- Extract renderer from App class to render directory (jkonecny)
|
||||
- Add tests for new ScreenStack and ScreenData (jkonecny)
|
||||
- Extract screen stack to its own object and data class (jkonecny)
|
||||
- Modify comments in the spec file (jkonecny)
|
||||
- Package is named python-simpleline (jkonecny)
|
||||
- Fix spec file (jkonecny)
|
||||
- Fix summary in the spec file (jkonecny)
|
||||
- Fix names according to tagging (jkonecny)
|
||||
|
||||
* Fri May 5 2017 Jiri Konecny <jkonecny@redhat.com> - 0.1-3
|
||||
- Modify comments in the spec file
|
||||
|
||||
* Thu May 4 2017 Jiri Konecny <jkonecny@redhat.com> - 0.1-2
|
||||
- Drop clean section
|
||||
- Drop Group, it is not needed
|
||||
- Use make_build macro
|
||||
- Reorder check and install sections
|
||||
- Rename package to python-simpleline but rpm will be python3-simpleline
|
||||
|
||||
* Fri Dec 16 2016 Jiri Konecny <jkonecny@redhat.com> - 0.1-1
|
||||
- Initial package
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/python3
|
||||
#
|
||||
# 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 setuptools import setup, find_packages
|
||||
|
||||
with open("README.md", "r") as f:
|
||||
long_description = f.read()
|
||||
|
||||
setup(name='simpleline',
|
||||
version='1.8',
|
||||
author='Jiri Konecny',
|
||||
author_email='jkonecny@redhat.com',
|
||||
description='Python text UI framework',
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
keywords='ui text library glib',
|
||||
url='https://github.com/rhinstaller/python-simpleline',
|
||||
packages=find_packages(include=['simpleline', 'simpleline.*']),
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: User Interfaces",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
|
||||
"Programming Language :: Python :: 3",
|
||||
],
|
||||
python_requires='>=3.4')
|
||||
+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
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
# Helper functions for the test classes.
|
||||
#
|
||||
# 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 import App
|
||||
from simpleline.global_configuration import DEFAULT_WIDTH
|
||||
|
||||
|
||||
class UtilityMixin():
|
||||
|
||||
@staticmethod
|
||||
def calculate_separator(width=DEFAULT_WIDTH):
|
||||
separator = "\n".join(2 * [width * "="])
|
||||
separator += "\n" # print adds another newline
|
||||
return separator
|
||||
|
||||
def create_output_with_separators(self, screens_text):
|
||||
msg = ""
|
||||
for screen_txt in screens_text:
|
||||
msg += self.calculate_separator()
|
||||
msg += screen_txt + "\n\n"
|
||||
|
||||
return msg
|
||||
|
||||
@staticmethod
|
||||
def schedule_screen_and_run(screen):
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
# Helper functions for the GLib test classes.
|
||||
#
|
||||
# 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 gi
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.event_loop.glib_event_loop import GLibEventLoop
|
||||
|
||||
gi.require_version("GLib", "2.0")
|
||||
|
||||
from gi.repository import GLib # pylint: disable=wrong-import-position, wrong-import-order
|
||||
|
||||
|
||||
class GLibUtilityMixin():
|
||||
|
||||
def __init__(self):
|
||||
self.loop = None
|
||||
self.timeout_error = False
|
||||
|
||||
def _quit_loop(self, loop):
|
||||
"""Kill GLib loop."""
|
||||
loop.quit()
|
||||
self.timeout_error = True
|
||||
return True
|
||||
|
||||
def create_glib_loop(self):
|
||||
# clear flags
|
||||
self.timeout_error = False
|
||||
self.loop = GLibEventLoop()
|
||||
|
||||
loop = self.loop.active_main_loop
|
||||
context = loop.get_context()
|
||||
|
||||
# This is prevention from running loop indefinitely
|
||||
source = GLib.timeout_source_new_seconds(2)
|
||||
source.set_callback(self._quit_loop, loop)
|
||||
source.attach(context)
|
||||
|
||||
def setup_glib(self):
|
||||
self.create_glib_loop()
|
||||
App.initialize(event_loop=self.loop)
|
||||
|
||||
def teardown_glib(self):
|
||||
if self.timeout_error:
|
||||
raise AssertionError("Loop was killed by timeout!")
|
||||
|
||||
def schedule_screen_and_run_with_glib(self, screen):
|
||||
self.setup_glib()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Event loop test classes for GLib implementation.
|
||||
#
|
||||
# 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 . import GLibUtilityMixin
|
||||
from ..main.event_loop_test import ProcessEvents_TestCase
|
||||
|
||||
|
||||
class GLibProcessEvents_TestCase(ProcessEvents_TestCase, GLibUtilityMixin):
|
||||
"""Run all the tests in ProcessEvents test case but with GLib event loop."""
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.teardown_glib()
|
||||
|
||||
def create_loop(self):
|
||||
self.create_glib_loop()
|
||||
|
||||
|
||||
# Hack to avoid running the original class thanks to import
|
||||
del ProcessEvents_TestCase
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# Rendering screen test classes.
|
||||
#
|
||||
# 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 . import GLibUtilityMixin
|
||||
from ..main.input_handler_test import InputHandler_TestCase
|
||||
|
||||
|
||||
class GLibInputHandler_TestCase(InputHandler_TestCase, GLibUtilityMixin):
|
||||
"""Run all the tests in InputHandler test case but with GLib event loop."""
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.teardown_glib()
|
||||
|
||||
def create_loop(self):
|
||||
self.create_glib_loop()
|
||||
|
||||
|
||||
# Hack to avoid running the original class thanks to import
|
||||
del InputHandler_TestCase
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
# Rendering screen test classes for GLib implementation.
|
||||
#
|
||||
# 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 . import GLibUtilityMixin
|
||||
from ..main.render_screen_test import SimpleUIScreenProcessing_TestCase, \
|
||||
InputProcessing_TestCase, ScreenException_TestCase
|
||||
|
||||
|
||||
class GLibSimpleUIScreenProcessing_TestCase(SimpleUIScreenProcessing_TestCase, GLibUtilityMixin):
|
||||
"""Run all the tests in ProcessEvents test case but with GLib event loop."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.loop = None
|
||||
self.timeout_error = False
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.teardown_glib()
|
||||
|
||||
def schedule_screen_and_run(self, screen):
|
||||
self.schedule_screen_and_run_with_glib(screen)
|
||||
|
||||
|
||||
# Hack to avoid running the original class thanks to import
|
||||
del SimpleUIScreenProcessing_TestCase
|
||||
|
||||
|
||||
class GLibScreenException_TestCase(ScreenException_TestCase, GLibUtilityMixin):
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.teardown_glib()
|
||||
|
||||
def schedule_screen_and_run(self, screen):
|
||||
self.schedule_screen_and_run_with_glib(screen)
|
||||
|
||||
|
||||
# Hack to avoid running the original class thanks to import
|
||||
del ScreenException_TestCase
|
||||
|
||||
|
||||
class GLibInputProcessing_TestCase(InputProcessing_TestCase, GLibUtilityMixin):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.setup_glib()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.teardown_glib()
|
||||
|
||||
|
||||
# Hack to avoid running the original class thanks to import
|
||||
del InputProcessing_TestCase
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Screen scheduling GLib test classes.
|
||||
#
|
||||
# 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 . import GLibUtilityMixin
|
||||
from ..main.screen_scheduler_test import ScreenScheduler_TestCase
|
||||
|
||||
|
||||
class GLibScreenScheduler_TestCase(ScreenScheduler_TestCase, GLibUtilityMixin):
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.teardown_glib()
|
||||
|
||||
def schedule_screen_and_run(self, screen):
|
||||
self.schedule_screen_and_run_with_glib(screen)
|
||||
|
||||
|
||||
# Hack to avoid running the original class thanks to import
|
||||
del ScreenScheduler_TestCase
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
# Advanced widgets test cases.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from unittest.mock import patch
|
||||
from io import StringIO
|
||||
|
||||
from simpleline.render.adv_widgets import GetInputScreen, GetPasswordInputScreen
|
||||
|
||||
from .. import UtilityMixin
|
||||
|
||||
|
||||
@patch('simpleline.input.input_handler.InputHandlerRequest._get_input')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
class AdvWidgets_TestCase(unittest.TestCase, UtilityMixin):
|
||||
def setUp(self):
|
||||
self.correct_input = False
|
||||
self.args_used = False
|
||||
|
||||
def test_gettext(self, stdout_mock, stdin_mock):
|
||||
prompt = "Type input"
|
||||
input_text = "user input"
|
||||
screen = GetInputScreen(message=prompt)
|
||||
stdin_mock.return_value = input_text
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
expected_output = self.create_output_with_separators(["%s: " % prompt]).rstrip('\n')
|
||||
|
||||
self.assertEqual(expected_output, stdout_mock.getvalue())
|
||||
self.assertEqual(screen.value, input_text)
|
||||
|
||||
def test_gettext_with_condition(self, stdout_mock, stdin_mock):
|
||||
prompt = "Type input"
|
||||
wrong_input = "wrong"
|
||||
condition = lambda x, _: x != wrong_input
|
||||
stdin_mock.side_effect = self.input_generator()
|
||||
|
||||
screen = GetInputScreen(message=prompt)
|
||||
screen.add_acceptance_condition(condition)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
temp_prompt = ["%s: %s: " % (prompt, prompt)]
|
||||
expected_output = self.create_output_with_separators(temp_prompt).rstrip("\n")
|
||||
|
||||
self.assertEqual(expected_output, stdout_mock.getvalue())
|
||||
self.assertTrue(self.correct_input)
|
||||
|
||||
def test_gettext_with_condition_and_use_arg(self, stdout_mock, stdin_mock):
|
||||
prompt = "Type input"
|
||||
user_input = "y"
|
||||
stdin_mock.return_value = user_input
|
||||
|
||||
screen = GetInputScreen(message=prompt)
|
||||
screen.add_acceptance_condition(self.acceptance_condition_test, "y")
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
expected_msg = "%s: " % prompt
|
||||
expected_output = self.create_output_with_separators([expected_msg]).rstrip("\n")
|
||||
|
||||
self.assertEqual(expected_output, stdout_mock.getvalue())
|
||||
self.assertTrue(self.args_used)
|
||||
|
||||
@patch("simpleline.global_configuration.GlobalConfiguration.password_function")
|
||||
def test_getpass(self, hiden_stdin_mock, stdout_mock, stdin_mock):
|
||||
prompt = "Type input"
|
||||
input_text = "user input"
|
||||
screen = GetPasswordInputScreen(message=prompt)
|
||||
hiden_stdin_mock.return_value = input_text
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertEqual(screen.value, input_text)
|
||||
|
||||
@patch("simpleline.global_configuration.GlobalConfiguration.password_function")
|
||||
def test_getpass_with_condition(self, hiden_stdin_mock, stdout_mock, stdin_mock):
|
||||
prompt = "Type input"
|
||||
wrong_input = "wrong"
|
||||
condition = lambda x, _: x != wrong_input
|
||||
hiden_stdin_mock.side_effect = self.input_generator()
|
||||
|
||||
screen = GetPasswordInputScreen(message=prompt)
|
||||
screen.add_acceptance_condition(condition)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertTrue(self.correct_input)
|
||||
|
||||
def input_generator(self):
|
||||
for i in ("wrong", "correct"):
|
||||
if i == "correct":
|
||||
self.correct_input = True
|
||||
yield i
|
||||
|
||||
def acceptance_condition_test(self, user_input, args):
|
||||
if user_input == args:
|
||||
self.args_used = True
|
||||
return True
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# App class test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.global_configuration import GlobalConfiguration
|
||||
from simpleline.input.input_threading import InputThreadManager
|
||||
from simpleline.render.screen_scheduler import ScreenScheduler
|
||||
from simpleline.event_loop.main_loop import MainLoop
|
||||
from simpleline.errors import NothingScheduledError
|
||||
|
||||
|
||||
class App_TestCase(unittest.TestCase):
|
||||
|
||||
def test_create_instance(self):
|
||||
App.initialize()
|
||||
self.assertTrue(isinstance(App.get_scheduler(), ScreenScheduler))
|
||||
self.assertTrue(isinstance(App.get_event_loop(), MainLoop))
|
||||
self.assertTrue(isinstance(App.get_configuration(), GlobalConfiguration))
|
||||
|
||||
def test_create_instance_with_custom_scheduler(self):
|
||||
App.initialize(scheduler=CustomScreenScheduler(CustomEventLoop()))
|
||||
self.assertTrue(isinstance(App.get_scheduler(), CustomScreenScheduler))
|
||||
|
||||
def test_create_instance_with_event_loop(self):
|
||||
App.initialize(event_loop=CustomEventLoop())
|
||||
self.assertTrue(isinstance(App.get_event_loop(), CustomEventLoop))
|
||||
|
||||
def test_create_instance_with_configuration(self):
|
||||
App.initialize(global_configuration=CustomGlobalConfiguration())
|
||||
self.assertTrue(isinstance(App.get_configuration(), CustomGlobalConfiguration))
|
||||
|
||||
def test_create_instance_with_custom_everything(self):
|
||||
event_loop = CustomEventLoop()
|
||||
App.initialize(event_loop=event_loop,
|
||||
scheduler=CustomScreenScheduler(event_loop),
|
||||
global_configuration=CustomGlobalConfiguration())
|
||||
|
||||
self.assertTrue(isinstance(App.get_event_loop(), CustomEventLoop))
|
||||
self.assertTrue(isinstance(App.get_scheduler(), CustomScreenScheduler))
|
||||
self.assertTrue(isinstance(App.get_configuration(), CustomGlobalConfiguration))
|
||||
|
||||
def test_reinitialize(self):
|
||||
event_loop1 = CustomEventLoop()
|
||||
event_loop2 = CustomEventLoop()
|
||||
scheduler1 = CustomScreenScheduler(event_loop1)
|
||||
scheduler2 = CustomScreenScheduler(event_loop2)
|
||||
configuration1 = CustomGlobalConfiguration()
|
||||
configuration2 = CustomGlobalConfiguration()
|
||||
|
||||
App.initialize(event_loop=event_loop1, scheduler=scheduler1,
|
||||
global_configuration=configuration1)
|
||||
self._check_app_settings(event_loop1, scheduler1, configuration1)
|
||||
|
||||
App.initialize(event_loop=event_loop2, scheduler=scheduler2,
|
||||
global_configuration=configuration2)
|
||||
self._check_app_settings(event_loop2, scheduler2, configuration2)
|
||||
|
||||
App.initialize()
|
||||
self.assertNotEqual(App.get_event_loop(), event_loop2)
|
||||
self.assertNotEqual(App.get_scheduler(), scheduler2)
|
||||
self.assertNotEqual(App.get_configuration(), configuration2)
|
||||
|
||||
def test_input_thread_manager_after_initialize(self):
|
||||
App.initialize()
|
||||
|
||||
thread_mgr = InputThreadManager.get_instance()
|
||||
|
||||
App.initialize()
|
||||
|
||||
self.assertNotEqual(thread_mgr, InputThreadManager.get_instance())
|
||||
|
||||
@mock.patch('simpleline.event_loop.main_loop.MainLoop.run')
|
||||
def test_run_shortcut(self, run_mock):
|
||||
App.initialize()
|
||||
App.get_configuration().should_run_with_empty_stack = True
|
||||
App.run()
|
||||
self.assertTrue(run_mock.called)
|
||||
|
||||
def test_run_with_empty_screen_stack(self):
|
||||
App.initialize()
|
||||
with self.assertRaises(NothingScheduledError):
|
||||
App.run()
|
||||
|
||||
def _check_app_settings(self, event_loop, scheduler, configuration):
|
||||
self.assertEqual(App.get_event_loop(), event_loop)
|
||||
self.assertEqual(App.get_scheduler(), scheduler)
|
||||
self.assertEqual(App.get_configuration(), configuration)
|
||||
|
||||
|
||||
class CustomScreenScheduler(ScreenScheduler):
|
||||
pass
|
||||
|
||||
|
||||
class CustomEventLoop(MainLoop):
|
||||
pass
|
||||
|
||||
|
||||
class CustomGlobalConfiguration(GlobalConfiguration):
|
||||
pass
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
# Containers test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.containers import WindowContainer, ListRowContainer, ListColumnContainer, \
|
||||
KeyPattern
|
||||
from simpleline.render.screen import UIScreen, InputState
|
||||
from simpleline.render.widgets import TextWidget
|
||||
|
||||
from .widgets_test import BaseWidgets_TestCase
|
||||
|
||||
|
||||
class Containers_TestCase(BaseWidgets_TestCase):
|
||||
|
||||
def _test_callback(self, data):
|
||||
pass
|
||||
|
||||
def test_listrow_container(self):
|
||||
c = ListRowContainer(columns=2,
|
||||
items=[self.w2, self.w3, self.w5],
|
||||
columns_width=10,
|
||||
spacing=2,
|
||||
numbering=False)
|
||||
c.render(25)
|
||||
|
||||
expected_result = [u"Test Test 2",
|
||||
u"Test 3"]
|
||||
res_lines = c.get_lines()
|
||||
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_empty(self):
|
||||
c = ListRowContainer(columns=1)
|
||||
|
||||
c.render(10)
|
||||
result = c.get_lines()
|
||||
|
||||
self.assertEqual(len(result), 0)
|
||||
|
||||
def test_more_columns_than_widgets(self):
|
||||
c = ListRowContainer(columns=3, items=[self.w1], columns_width=40, numbering=False)
|
||||
c.render(80)
|
||||
|
||||
expected_result = [u"Můj krásný dlouhý text"]
|
||||
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_listrow_wrapping(self):
|
||||
# spacing is 3 by default
|
||||
c = ListRowContainer(2,
|
||||
[self.w1, self.w2, self.w3, self.w4],
|
||||
columns_width=15,
|
||||
numbering=False)
|
||||
c.render(25)
|
||||
|
||||
expected_result = [u"Můj krásný Test",
|
||||
u"dlouhý text",
|
||||
u"Test 2 Krásný dlouhý",
|
||||
u" text podruhé"]
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_newline_wrapping(self):
|
||||
widgets = [TextWidget("Hello"), TextWidget("Wrap\nthis\ntext"), TextWidget("Hi"),
|
||||
TextWidget("Hello2")]
|
||||
|
||||
c = ListRowContainer(3, widgets, columns_width=6, spacing=1, numbering=False)
|
||||
c.render(80)
|
||||
|
||||
expected_result = [u"Hello Wrap Hi",
|
||||
u" this",
|
||||
u" text",
|
||||
u"Hello2"]
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_listcolumn_container(self):
|
||||
c = ListColumnContainer(columns=2,
|
||||
items=[self.w2, self.w3, self.w5],
|
||||
columns_width=10,
|
||||
spacing=2,
|
||||
numbering=False)
|
||||
c.render(25)
|
||||
|
||||
expected_result = [u"Test Test 3",
|
||||
u"Test 2"]
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_listcolumn_wrapping(self):
|
||||
# spacing is 3 by default
|
||||
c = ListColumnContainer(2,
|
||||
[self.w1, self.w2, self.w3, self.w4],
|
||||
columns_width=15,
|
||||
numbering=False)
|
||||
c.render(25)
|
||||
|
||||
expected_result = [u"Můj krásný Test 2",
|
||||
u"dlouhý text",
|
||||
u"Test Krásný dlouhý",
|
||||
u" text podruhé"]
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_add_new_container(self):
|
||||
c = ListRowContainer(columns=2,
|
||||
items=[TextWidget("Ahoj")],
|
||||
columns_width=15,
|
||||
spacing=0,
|
||||
numbering=False)
|
||||
|
||||
expected_result = [u"Ahoj"]
|
||||
|
||||
c.render(80)
|
||||
self.evaluate_result(c.get_lines(), expected_result)
|
||||
|
||||
c.add(TextWidget("Nový widget"))
|
||||
c.add(TextWidget("Hello"))
|
||||
|
||||
expected_result = [u"Ahoj Nový widget",
|
||||
u"Hello"]
|
||||
|
||||
c.render(80)
|
||||
self.evaluate_result(c.get_lines(), expected_result)
|
||||
|
||||
def test_column_numbering(self):
|
||||
# spacing is 3 by default
|
||||
c = ListColumnContainer(2, [self.w1, self.w2, self.w3, self.w4], columns_width=16)
|
||||
c.render(25)
|
||||
|
||||
expected_result = [u"1) Můj krásný 3) Test 2",
|
||||
u" dlouhý text",
|
||||
u"2) Test 4) Krásný dlouhý",
|
||||
u" text podruhé"]
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_row_numbering(self):
|
||||
# spacing is 3 by default
|
||||
c = ListRowContainer(2, [self.w1, self.w2, self.w3, self.w4], columns_width=16)
|
||||
c.render(25)
|
||||
|
||||
expected_result = [u"1) Můj krásný 2) Test",
|
||||
u" dlouhý text",
|
||||
u"3) Test 2 4) Krásný dlouhý",
|
||||
u" text podruhé"]
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_custom_numbering(self):
|
||||
# spacing is 3 by default
|
||||
c = ListRowContainer(2, [self.w1, self.w2, self.w3, self.w4], columns_width=20)
|
||||
c.key_pattern = KeyPattern("a {:d} a ")
|
||||
c.render(25)
|
||||
|
||||
expected_result = [u"a 1 a Můj krásný a 2 a Test",
|
||||
u" dlouhý text",
|
||||
u"a 3 a Test 2 a 4 a Krásný dlouhý",
|
||||
u" text podruhé"]
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_window_container(self):
|
||||
c = WindowContainer(title="Test")
|
||||
|
||||
c.add(TextWidget("Body"))
|
||||
c.render(10)
|
||||
|
||||
expected_result = [u"Test",
|
||||
u"",
|
||||
u"Body"]
|
||||
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_window_container_with_multiple_items(self):
|
||||
c = WindowContainer(title="Test")
|
||||
|
||||
c.add(TextWidget("Body"))
|
||||
c.add(TextWidget("Body second line"))
|
||||
c.render(30)
|
||||
|
||||
expected_result = [u"Test",
|
||||
u"",
|
||||
u"Body",
|
||||
u"Body second line"]
|
||||
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_window_container_wrapping(self):
|
||||
c = WindowContainer(title="Test")
|
||||
|
||||
c.add(TextWidget("Body long line"))
|
||||
c.add(TextWidget("Body"))
|
||||
c.render(5)
|
||||
|
||||
expected_result = [u"Test",
|
||||
u"",
|
||||
u"Body",
|
||||
u"long",
|
||||
u"line",
|
||||
u"Body"]
|
||||
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_list_container_without_width(self):
|
||||
column_count = 3
|
||||
spacing_width = 3
|
||||
c = ListRowContainer(column_count, spacing=spacing_width, numbering=False)
|
||||
|
||||
c.add(TextWidget("AAAA"))
|
||||
c.add(TextWidget("BBBB"))
|
||||
c.add(TextWidget("CCCCC")) # this line is too long
|
||||
c.add(TextWidget("DDDD"))
|
||||
|
||||
expected_col_width = 4
|
||||
expected_spacing_sum = 2 * spacing_width # three columns so 2 spacing between them
|
||||
render_width = (column_count * expected_col_width) + expected_spacing_sum
|
||||
c.render(render_width)
|
||||
|
||||
expected_result = [u"AAAA BBBB CCCC",
|
||||
u" C",
|
||||
u"DDDD"]
|
||||
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_list_container_too_small(self):
|
||||
# to be able to render this container we need at least 11 width
|
||||
# 8 will take only spacing and then 1 for every column
|
||||
c = ListRowContainer(3, spacing=4, numbering=False)
|
||||
|
||||
c.add(TextWidget("This can't be rendered."))
|
||||
c.add(TextWidget("Because spacing takes more space than maximal width."))
|
||||
c.add(TextWidget("Exception will raise."))
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "Columns width is too small."):
|
||||
c.render(10)
|
||||
|
||||
def test_list_container_too_small_turn_off_numbering(self):
|
||||
# to be able to render this container we need
|
||||
# 11 width + three times numbers (3 characters) = 20
|
||||
#
|
||||
# 8 will take only spacing and then 1 for every column
|
||||
c = ListRowContainer(3, spacing=4, numbering=True)
|
||||
|
||||
c.add(TextWidget("This can't be rendered."))
|
||||
c.add(TextWidget("Because spacing takes more space than maximal width."))
|
||||
c.add(TextWidget("Exception will raise with info to turn off numbering."))
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "Increase column width or disable numbering."):
|
||||
c.render(19)
|
||||
|
||||
|
||||
@patch('simpleline.input.input_handler.InputHandlerRequest._get_input')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
class ContainerInput_TestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._callback_id = None
|
||||
self._callback_called = None
|
||||
|
||||
def _prepare_callbacks(self, container, count):
|
||||
for i in range(count):
|
||||
container.add(TextWidget("Test"), self._callback, i + 1)
|
||||
|
||||
def test_list_widget_input_processing(self, out_mock, in_mock):
|
||||
# call first container callback
|
||||
in_mock.return_value = "2"
|
||||
|
||||
screen = ScreenWithListWidget(3)
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(1, screen.container_callback_input)
|
||||
|
||||
# TEST 0 or less as user input
|
||||
|
||||
def test_list_input_processing_input_0(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
self._prepare_callbacks(c, 3)
|
||||
|
||||
self.assertFalse(c.process_user_input("0"))
|
||||
|
||||
def test_list_input_processing_negative_number(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
self._prepare_callbacks(c, 3)
|
||||
|
||||
self.assertFalse(c.process_user_input("-2"))
|
||||
|
||||
def test_list_input_processing_exceeded(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
self._prepare_callbacks(c, 2)
|
||||
|
||||
self.assertFalse(c.process_user_input("3"))
|
||||
|
||||
def test_list_without_callback(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
c.add(TextWidget("Test"))
|
||||
|
||||
self.assertTrue(c.process_user_input("1"))
|
||||
|
||||
def test_list_callback_without_data(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
c.add(TextWidget("Test"), self._callback)
|
||||
|
||||
self.assertTrue(c.process_user_input("1"))
|
||||
self.assertIsNone(self._callback_called)
|
||||
|
||||
def test_list_correct_input_processing(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
self._prepare_callbacks(c, 3)
|
||||
|
||||
self.assertTrue(c.process_user_input("2"))
|
||||
|
||||
self.assertEqual(self._callback_called, 2)
|
||||
|
||||
def test_list_wrong_input_processing(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
self._prepare_callbacks(c, 3)
|
||||
|
||||
self.assertFalse(c.process_user_input("c"))
|
||||
|
||||
def test_list_input_processing_none(self, out_mock, in_mock):
|
||||
c = ListRowContainer(1)
|
||||
|
||||
self._prepare_callbacks(c, 2)
|
||||
|
||||
self.assertFalse(c.process_user_input(None))
|
||||
|
||||
def _callback(self, data):
|
||||
self._callback_called = data
|
||||
|
||||
|
||||
class ScreenWithListWidget(UIScreen):
|
||||
|
||||
def __init__(self, widgets_count):
|
||||
super().__init__()
|
||||
self._widgets_count = widgets_count
|
||||
self._list_widget = None
|
||||
self.container_callback_input = -1
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
|
||||
self._list_widget = ListRowContainer(2)
|
||||
for i in range(self._widgets_count):
|
||||
self._list_widget.add(TextWidget("Test %s" % i), self._callback, i)
|
||||
|
||||
self.window.add(self._list_widget)
|
||||
|
||||
def input(self, args, key):
|
||||
self.close()
|
||||
if self._list_widget.process_user_input(key):
|
||||
return InputState.PROCESSED
|
||||
|
||||
return InputState.DISCARDED
|
||||
|
||||
def _callback(self, data):
|
||||
self.container_callback_input = data
|
||||
+310
@@ -0,0 +1,310 @@
|
||||
# Event loop test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from simpleline.event_loop import AbstractSignal
|
||||
from simpleline.event_loop import EventHandler
|
||||
from simpleline.event_loop import ExitMainLoop
|
||||
from simpleline.event_loop.main_loop import MainLoop
|
||||
|
||||
|
||||
class EventLoopHandler_TestCase(unittest.TestCase):
|
||||
|
||||
def callback_func(self):
|
||||
pass
|
||||
|
||||
def test_signal_handler_named_params(self):
|
||||
data = [1, 2, "args"]
|
||||
ev = EventHandler(callback=self.callback_func, data=data)
|
||||
|
||||
self.assertEqual(ev.callback, self.callback_func)
|
||||
self.assertEqual(ev.data, data)
|
||||
|
||||
def test_signal_handler_positional_params(self):
|
||||
data = [1, 3, "args"]
|
||||
ev = EventHandler(self.callback_func, data)
|
||||
|
||||
self.assertEqual(ev.callback, self.callback_func)
|
||||
self.assertEqual(ev.data, data)
|
||||
|
||||
|
||||
class ProcessEvents_TestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.signal_counter = 0
|
||||
self.signal_counter2 = 0
|
||||
self.signal_counter_copied = 0
|
||||
self.callback_called = False
|
||||
self.callback_args = None
|
||||
self.create_loop()
|
||||
|
||||
def create_loop(self):
|
||||
self.loop = MainLoop()
|
||||
|
||||
def test_simple_register_handler(self):
|
||||
self.callback_called = False
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_callback)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.process_signals()
|
||||
|
||||
self.assertTrue(self.callback_called)
|
||||
|
||||
def test_process_more_signals(self):
|
||||
self.signal_counter = 0
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_signal_counter)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.process_signals()
|
||||
|
||||
self.assertEqual(self.signal_counter, 3)
|
||||
|
||||
def test_process_signals_multiple_times(self):
|
||||
self.signal_counter = 0
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_signal_counter)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.process_signals()
|
||||
self.assertEqual(self.signal_counter, 2)
|
||||
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.process_signals()
|
||||
self.assertEqual(self.signal_counter, 4)
|
||||
|
||||
def test_wait_on_signal(self):
|
||||
self.signal_counter = 0
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal,
|
||||
self._handler_signal_counter)
|
||||
loop.register_signal_handler(TestSignal2,
|
||||
self._handler_process_events_then_register_testsignal,
|
||||
loop)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal2())
|
||||
loop.process_signals(return_after=TestSignal2)
|
||||
self.assertEqual(self.signal_counter, 1)
|
||||
|
||||
loop.process_signals()
|
||||
self.assertEqual(self.signal_counter, 2)
|
||||
|
||||
def test_wait_on_signal_skipped_by_inner_process_events(self):
|
||||
self.signal_counter = 0
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal,
|
||||
self._handler_signal_counter)
|
||||
# run process signals recursively in this handler which will skip processing
|
||||
loop.register_signal_handler(TestSignal2,
|
||||
self._handler_process_events_then_register_testsignal,
|
||||
loop)
|
||||
loop.enqueue_signal(TestSignal2())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
# new signal will be registered in handler method but that shouldn't be processed
|
||||
# because it should end on the first signal even when it was skipped
|
||||
loop.process_signals(return_after=TestSignal)
|
||||
|
||||
self.assertEqual(self.signal_counter, 1)
|
||||
|
||||
def test_multiple_handlers_to_signal(self):
|
||||
self.signal_counter = 0
|
||||
self.signal_counter2 = 0
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_signal_counter)
|
||||
loop.register_signal_handler(TestSignal, self._handler_signal_counter2)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.process_signals()
|
||||
|
||||
self.assertEqual(self.signal_counter, 2)
|
||||
self.assertEqual(self.signal_counter2, 2)
|
||||
|
||||
def test_priority_signal_processing(self):
|
||||
self.signal_counter = 0
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_signal_counter)
|
||||
loop.register_signal_handler(TestPrioritySignal, self._handler_signal_counter)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
# should be processed as first signal because of priority
|
||||
loop.enqueue_signal(TestPrioritySignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.process_signals()
|
||||
self.assertEqual(self.signal_counter, 1)
|
||||
|
||||
# process rest of the signals
|
||||
loop.process_signals()
|
||||
self.assertEqual(self.signal_counter, 4)
|
||||
|
||||
def test_low_priority_signal_processing(self):
|
||||
self.signal_counter = 0
|
||||
self.signal_counter_copied = 0
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_signal_counter)
|
||||
loop.register_signal_handler(TestLowPrioritySignal, self._handler_signal_copy_counter)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestLowPrioritySignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.process_signals()
|
||||
self.assertEqual(self.signal_counter, 3)
|
||||
|
||||
# process the low priority signal
|
||||
loop.process_signals()
|
||||
self.assertEqual(self.signal_counter_copied, 3)
|
||||
|
||||
def test_quit_callback(self):
|
||||
self.callback_called = False
|
||||
self.callback_args = None
|
||||
msg = "Test data"
|
||||
|
||||
loop = self.loop
|
||||
loop.set_quit_callback(self._handler_quit_callback, args=msg)
|
||||
loop.register_signal_handler(TestSignal, self._handler_raise_ExitMainLoop_exception)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.run()
|
||||
|
||||
self.assertTrue(self.callback_called)
|
||||
self.assertEqual(msg, self.callback_args)
|
||||
|
||||
def test_force_quit(self):
|
||||
self.callback_called = False
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_callback)
|
||||
loop.register_signal_handler(TestSignal2, self._handler_force_quit_exception)
|
||||
loop.enqueue_signal(TestSignal2())
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.run()
|
||||
|
||||
self.assertFalse(self.callback_called)
|
||||
|
||||
def test_force_quit_recursive_loop(self):
|
||||
self.callback_called = False
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal,
|
||||
self._handler_start_inner_loop_and_enqueue_event,
|
||||
TestSignal3())
|
||||
loop.register_signal_handler(TestSignal2,
|
||||
self._handler_callback)
|
||||
loop.register_signal_handler(TestSignal3,
|
||||
self._handler_force_quit_exception)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal2())
|
||||
loop.run()
|
||||
|
||||
self.assertFalse(self.callback_called)
|
||||
|
||||
def test_force_quit_when_waiting_on_signal(self):
|
||||
self.callback_called = False
|
||||
|
||||
loop = self.loop
|
||||
loop.register_signal_handler(TestSignal, self._handler_force_quit_exception)
|
||||
loop.register_signal_handler(TestSignal2, self._handler_callback)
|
||||
loop.enqueue_signal(TestSignal())
|
||||
loop.enqueue_signal(TestSignal2())
|
||||
|
||||
# FIXME: Find a better way how to detect infinite loop
|
||||
# if force quit won't work properly this will hang up
|
||||
loop.process_signals(return_after=TestSignal3)
|
||||
|
||||
self.assertFalse(self.callback_called)
|
||||
|
||||
# HANDLERS FOR TESTING
|
||||
def _handler_callback(self, signal, data):
|
||||
self.callback_called = True
|
||||
|
||||
def _handler_quit_callback(self, args):
|
||||
self.callback_called = True
|
||||
self.callback_args = args
|
||||
|
||||
def _handler_signal_counter(self, signal, data):
|
||||
self.signal_counter += 1
|
||||
|
||||
def _handler_signal_counter2(self, signal, data):
|
||||
self.signal_counter2 += 1
|
||||
|
||||
def _handler_signal_copy_counter(self, signal, data):
|
||||
self.signal_counter_copied = self.signal_counter
|
||||
|
||||
@staticmethod
|
||||
def _handler_process_events_then_register_testsignal(signal, data):
|
||||
event_loop = data
|
||||
event_loop.process_signals()
|
||||
# This shouldn't be processed
|
||||
event_loop.enqueue_signal(TestSignal())
|
||||
|
||||
def _handler_start_inner_loop_and_enqueue_event(self, signal, data):
|
||||
self.loop.execute_new_loop(data)
|
||||
|
||||
@staticmethod
|
||||
def _handler_raise_ExitMainLoop_exception(signal, data):
|
||||
raise ExitMainLoop()
|
||||
|
||||
def _handler_force_quit_exception(self, signal, data):
|
||||
self.loop.force_quit()
|
||||
|
||||
|
||||
# TESTING EVENTS
|
||||
class TestSignal(AbstractSignal):
|
||||
|
||||
def __init__(self):
|
||||
# ignore source
|
||||
super().__init__(None)
|
||||
|
||||
|
||||
class TestSignal2(AbstractSignal):
|
||||
|
||||
def __init__(self):
|
||||
# ignore source
|
||||
super().__init__(None)
|
||||
|
||||
|
||||
class TestSignal3(AbstractSignal):
|
||||
|
||||
def __init__(self):
|
||||
# ignore source
|
||||
super().__init__(None)
|
||||
|
||||
|
||||
class TestPrioritySignal(AbstractSignal):
|
||||
|
||||
def __init__(self):
|
||||
# ignore source
|
||||
super().__init__(None, -10)
|
||||
|
||||
|
||||
class TestLowPrioritySignal(AbstractSignal):
|
||||
|
||||
def __init__(self):
|
||||
# ignore source
|
||||
super().__init__(None, 20)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
# Event queue test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from simpleline.event_loop.event_queue import EventQueue, EventQueueError
|
||||
from simpleline.event_loop.signals import AbstractSignal
|
||||
|
||||
|
||||
class EventQueue_TestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.e = EventQueue()
|
||||
|
||||
def test_queue_is_empty(self):
|
||||
self.assertTrue(self.e.empty())
|
||||
|
||||
def test_enqueue(self):
|
||||
fake_signal = MagicMock()
|
||||
|
||||
self.e.enqueue(fake_signal)
|
||||
self.assertFalse(self.e.empty())
|
||||
|
||||
self.assertEqual(fake_signal, self.e.get())
|
||||
self.assertTrue(self.e.empty())
|
||||
|
||||
def test_enqueue_priority(self):
|
||||
signal_low_priority = TestSignal(priority=10)
|
||||
signal_high_priority = TestSignal(priority=0)
|
||||
|
||||
self.e.enqueue(signal_low_priority)
|
||||
self.e.enqueue(signal_high_priority)
|
||||
|
||||
self.assertEqual(signal_high_priority, self.e.get())
|
||||
self.assertEqual(signal_low_priority, self.e.get())
|
||||
|
||||
# Test adding signals in different order (result shouldn't change)
|
||||
self.e.enqueue(signal_high_priority)
|
||||
self.e.enqueue(signal_low_priority)
|
||||
|
||||
self.assertEqual(signal_high_priority, self.e.get())
|
||||
self.assertEqual(signal_low_priority, self.e.get())
|
||||
|
||||
def test_adding_event_source(self):
|
||||
fake_source = MagicMock()
|
||||
self.e.add_source(fake_source)
|
||||
|
||||
self.assertTrue(self.e.contains_source(fake_source))
|
||||
|
||||
def test_removing_event_source(self):
|
||||
fake_source = MagicMock()
|
||||
self.e.add_source(fake_source)
|
||||
|
||||
self.e.remove_source(fake_source)
|
||||
|
||||
self.assertFalse(self.e.contains_source(fake_source))
|
||||
|
||||
def test_remove_empty_source(self):
|
||||
with self.assertRaises(EventQueueError):
|
||||
self.e.remove_source(MagicMock())
|
||||
|
||||
def test_enqueue_if_source_belongs(self):
|
||||
source = MagicMock()
|
||||
signal = TestSignal(source=source)
|
||||
|
||||
self.e.add_source(source)
|
||||
self.assertTrue(self.e.enqueue_if_source_belongs(signal, source))
|
||||
self.assertEqual(signal, self.e.get())
|
||||
|
||||
def test_enqueue_if_source_does_not_belong(self):
|
||||
signal = TestSignal()
|
||||
signal_low_priority = TestSignal(priority=25)
|
||||
|
||||
# the get method will wait if nothing present so adding low priority signal below
|
||||
# give us check if the queue is really empty
|
||||
self.e.enqueue(signal_low_priority)
|
||||
|
||||
self.assertFalse(self.e.enqueue_if_source_belongs(signal, MagicMock()))
|
||||
self.assertEqual(signal_low_priority, self.e.get())
|
||||
|
||||
|
||||
class TestSignal(AbstractSignal):
|
||||
|
||||
def __init__(self, source=None, priority=20): # pylint: disable=useless-super-delegation
|
||||
super().__init__(source, priority)
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# GlobalConfiguration class test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.global_configuration import DEFAULT_WIDTH, DEFAULT_PASSWORD_FUNC
|
||||
|
||||
|
||||
class GlobalConfiguration_TestCase(unittest.TestCase):
|
||||
|
||||
def tearDown(self):
|
||||
App.initialize()
|
||||
|
||||
def _check_default_width(self, width=DEFAULT_WIDTH):
|
||||
self.assertEqual(App.get_configuration().width, width)
|
||||
|
||||
def _check_default_password_function(self, password_func=None):
|
||||
if password_func:
|
||||
self.assertEqual(App.get_configuration().password_function, password_func)
|
||||
else:
|
||||
self.assertEqual(App.get_configuration().password_function, DEFAULT_PASSWORD_FUNC)
|
||||
|
||||
def test_clear_width(self):
|
||||
self._check_default_width()
|
||||
|
||||
test_width = 150
|
||||
|
||||
App.get_configuration().width = test_width
|
||||
self._check_default_width(test_width)
|
||||
|
||||
App.get_configuration().clear_width()
|
||||
self._check_default_width()
|
||||
|
||||
def test_width(self):
|
||||
self._check_default_width()
|
||||
|
||||
App.initialize()
|
||||
App.get_configuration().width = 100
|
||||
self._check_default_width(100)
|
||||
|
||||
App.initialize()
|
||||
self._check_default_width()
|
||||
|
||||
def test_password_function(self):
|
||||
self._check_default_password_function()
|
||||
|
||||
test_mock = MagicMock()
|
||||
App.initialize()
|
||||
App.get_configuration().password_function = test_mock
|
||||
self._check_default_password_function(test_mock)
|
||||
|
||||
App.initialize()
|
||||
self._check_default_password_function()
|
||||
|
||||
def test_clear_password_function(self):
|
||||
self._check_default_password_function()
|
||||
|
||||
test_func = MagicMock()
|
||||
|
||||
App.get_configuration().password_function = test_func
|
||||
self._check_default_password_function(test_func)
|
||||
|
||||
App.get_configuration().clear_password_function()
|
||||
self._check_default_password_function()
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
# Rendering screen test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from threading import Barrier, current_thread, Event
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.event_loop.main_loop import MainLoop
|
||||
from simpleline.input.input_handler import InputHandler, PasswordInputHandler
|
||||
from simpleline.render.prompt import Prompt
|
||||
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
@patch('simpleline.input.input_handler.InputHandlerRequest._get_input')
|
||||
class InputHandler_TestCase(unittest.TestCase):
|
||||
|
||||
def create_loop(self):
|
||||
self.loop = MainLoop()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.create_loop()
|
||||
App.initialize(event_loop=self.loop)
|
||||
|
||||
self._callback_called = False
|
||||
self._callback_input = ""
|
||||
|
||||
self._callback_called2 = False
|
||||
self._callback_input2 = ""
|
||||
|
||||
self._thread_barrier = Barrier(2, timeout=3)
|
||||
self._thread_event_wait_for_inner = Event()
|
||||
self._thread_event_wait_for_outer = Event()
|
||||
self._threads = []
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self._thread_event_wait_for_outer.set()
|
||||
|
||||
for t in self._threads:
|
||||
t.join()
|
||||
|
||||
# process InputReceivedSignal
|
||||
App.get_event_loop().process_signals()
|
||||
# process InputReadySignal
|
||||
App.get_event_loop().process_signals()
|
||||
|
||||
def test_async_input(self, input_mock, output_mock):
|
||||
input_mock.return_value = 'a'
|
||||
|
||||
h = InputHandler()
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
h.wait_on_input()
|
||||
|
||||
self.assertEqual(h.value, 'a')
|
||||
|
||||
def test_input_received(self, input_mock, output_mock):
|
||||
input_mock.return_value = 'a'
|
||||
|
||||
h = InputHandler()
|
||||
|
||||
self.assertFalse(h.input_received())
|
||||
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
h.wait_on_input()
|
||||
|
||||
self.assertTrue(h.input_received())
|
||||
|
||||
def test_input_callback(self, input_mock, output_mock):
|
||||
input_value = 'abc'
|
||||
input_mock.return_value = input_value
|
||||
|
||||
h = InputHandler()
|
||||
h.set_callback(self._test_callback)
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
h.wait_on_input()
|
||||
|
||||
self.assertTrue(self._callback_called)
|
||||
self.assertEqual(self._callback_input, input_value)
|
||||
self.assertEqual(h.value, input_value)
|
||||
|
||||
def test_concurrent_input(self, input_mock, output_mock):
|
||||
input_mock.side_effect = self._wait_for_concurrent_call
|
||||
|
||||
h = InputHandler()
|
||||
h.set_callback(self._test_callback)
|
||||
h2 = InputHandler()
|
||||
h2.set_callback(self._test_callback2)
|
||||
|
||||
with self.assertRaisesRegex(KeyError, r'.*Input handler:.*InputHandler object'
|
||||
r'.*Input requester: Unknown'
|
||||
r'.*Input handler:.*InputHandler object'
|
||||
r'.*Input requester: Unknown.*'):
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
self._thread_event_wait_for_inner.wait()
|
||||
h2.get_input(Prompt(message="ABC"))
|
||||
|
||||
self.assertFalse(self._callback_called)
|
||||
self.assertEqual(self._callback_input, "")
|
||||
self.assertEqual(h.value, None)
|
||||
|
||||
self.assertFalse(self._callback_called2)
|
||||
self.assertEqual(self._callback_input2, "")
|
||||
self.assertEqual(h2.value, None)
|
||||
|
||||
def test_concurrent_input_with_source(self, input_mock, output_mock):
|
||||
input_mock.side_effect = self._wait_for_concurrent_call
|
||||
source1 = Mock()
|
||||
source2 = Mock()
|
||||
|
||||
h = InputHandler(source=source1)
|
||||
h2 = InputHandler(source=source2)
|
||||
h.set_callback(self._test_callback)
|
||||
h2.set_callback(self._test_callback2)
|
||||
|
||||
with self.assertRaisesRegex(KeyError, r'.*Input handler:.*InputHandler object'
|
||||
r'.*Input requester:.*Mock'
|
||||
r'.*Input handler:.*InputHandler object'
|
||||
r'.*Input requester:.*Mock.*'):
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
self._thread_event_wait_for_inner.wait()
|
||||
h2.get_input(Prompt(message="ABC"))
|
||||
|
||||
self.assertFalse(self._callback_called)
|
||||
self.assertEqual(self._callback_input, "")
|
||||
self.assertEqual(h.value, None)
|
||||
|
||||
self.assertFalse(self._callback_called2)
|
||||
self.assertEqual(self._callback_input2, "")
|
||||
self.assertEqual(h2.value, None)
|
||||
|
||||
def test_concurrent_input_without_check(self, input_mock, output_mock):
|
||||
input_mock.side_effect = self._wait_for_concurrent_call
|
||||
|
||||
h = InputHandler()
|
||||
h2 = InputHandler()
|
||||
h.set_callback(self._test_callback)
|
||||
h2.set_callback(self._test_callback2)
|
||||
h2.skip_concurrency_check = True
|
||||
h.skip_concurrency_check = True
|
||||
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
self._thread_event_wait_for_inner.wait()
|
||||
h2.get_input(Prompt(message="ABC"))
|
||||
self._thread_event_wait_for_outer.set()
|
||||
|
||||
h.wait_on_input()
|
||||
h2.wait_on_input()
|
||||
|
||||
self.assertFalse(self._callback_called)
|
||||
self.assertFalse(h.input_successful())
|
||||
|
||||
self.assertTrue(self._callback_called2)
|
||||
self.assertTrue(h2.input_successful())
|
||||
self.assertEqual(self._callback_input2, "thread 0")
|
||||
self.assertEqual(h2.value, "thread 0")
|
||||
|
||||
def _wait_for_concurrent_call(self):
|
||||
ret = "thread {}".format(len(self._threads))
|
||||
self._threads.append(current_thread())
|
||||
self._thread_event_wait_for_inner.set()
|
||||
self._thread_event_wait_for_outer.wait()
|
||||
return ret
|
||||
|
||||
def _test_callback(self, user_input):
|
||||
self._callback_called = True
|
||||
self._callback_input = user_input
|
||||
|
||||
def _test_callback2(self, user_input):
|
||||
self._callback_called2 = True
|
||||
self._callback_input2 = user_input
|
||||
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
@patch('simpleline.global_configuration.GlobalConfiguration.password_function')
|
||||
class PasswordInputHandler_TestCase(unittest.TestCase):
|
||||
|
||||
def create_loop(self):
|
||||
self.loop = MainLoop()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.create_loop()
|
||||
App.initialize(event_loop=self.loop)
|
||||
|
||||
self._callback_called = False
|
||||
self._callback_input = ""
|
||||
|
||||
def test_async_input(self, input_mock, output_mock):
|
||||
input_mock.return_value = 'a'
|
||||
|
||||
h = PasswordInputHandler()
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
h.wait_on_input()
|
||||
|
||||
self.assertEqual(h.value, 'a')
|
||||
|
||||
def test_input_received(self, input_mock, output_mock):
|
||||
input_mock.return_value = 'a'
|
||||
|
||||
h = PasswordInputHandler()
|
||||
|
||||
self.assertFalse(h.input_received())
|
||||
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
h.wait_on_input()
|
||||
|
||||
self.assertTrue(h.input_received())
|
||||
|
||||
def test_input_callback(self, input_mock, output_mock):
|
||||
input_value = 'abc'
|
||||
input_mock.return_value = input_value
|
||||
|
||||
h = PasswordInputHandler()
|
||||
h.set_callback(self._test_callback)
|
||||
h.get_input(Prompt(message="ABC"))
|
||||
h.wait_on_input()
|
||||
|
||||
self.assertTrue(self._callback_called)
|
||||
self.assertEqual(self._callback_input, input_value)
|
||||
self.assertEqual(h.value, input_value)
|
||||
|
||||
def _test_callback(self, user_input):
|
||||
self._callback_called = True
|
||||
self._callback_input = user_input
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
# Prompt test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from simpleline.render.prompt import Prompt
|
||||
|
||||
|
||||
class Prompt_TestCase(unittest.TestCase):
|
||||
def test_prompt_message(self):
|
||||
p = Prompt()
|
||||
|
||||
self.assertEqual(p.message, Prompt.DEFAULT_MESSAGE)
|
||||
|
||||
p.set_message("Brand new message")
|
||||
self.assertEqual(p.message, "Brand new message")
|
||||
|
||||
p.set_message(u"Žluťoučký kůň")
|
||||
self.assertEqual(p.message, u"Žluťoučký kůň")
|
||||
|
||||
p2 = Prompt("Default prompt text")
|
||||
self.assertEqual(p2.message, "Default prompt text")
|
||||
|
||||
def test_add_options(self):
|
||||
p = Prompt()
|
||||
|
||||
# add new option
|
||||
p.add_option("R", "refresh")
|
||||
self.assertTrue("R" in p.options)
|
||||
self.assertEqual(p.options["R"], "refresh")
|
||||
|
||||
# add option over the existing option should trigger warning message
|
||||
with self.assertLogs("simpleline", level="WARNING"):
|
||||
p.add_option("R", "another refresh")
|
||||
|
||||
# update existing option
|
||||
p.update_option("R", "new refresh option")
|
||||
self.assertEqual(p.options["R"], "new refresh option")
|
||||
|
||||
# update non existing option should trigger warning
|
||||
with self.assertLogs("simpleline", level="WARNING"):
|
||||
p.update_option("N", "non existing option")
|
||||
|
||||
p = Prompt()
|
||||
p.add_option("N", "new option")
|
||||
# remove option
|
||||
ret = p.remove_option("N")
|
||||
self.assertFalse("N" in p.options)
|
||||
self.assertEqual(ret, "new option")
|
||||
|
||||
# remove non-existing option
|
||||
ret = p.remove_option("non-existing")
|
||||
self.assertIsNone(ret)
|
||||
|
||||
def _check_default_option(self, prompt, key, value):
|
||||
self.assertEqual(len(prompt.options), 1)
|
||||
self.assertEqual(prompt.options[key], value)
|
||||
|
||||
def test_refresh_option(self):
|
||||
# refresh option
|
||||
p = Prompt()
|
||||
p.add_refresh_option()
|
||||
self._check_default_option(p, Prompt.REFRESH, Prompt.REFRESH_DESCRIPTION)
|
||||
|
||||
# test add with description
|
||||
p = Prompt()
|
||||
p.add_refresh_option("Other refresh")
|
||||
self._check_default_option(p, Prompt.REFRESH, "Other refresh")
|
||||
|
||||
# change existing description
|
||||
p.add_refresh_option("New refresh")
|
||||
self._check_default_option(p, Prompt.REFRESH, "New refresh")
|
||||
|
||||
def test_continue_option(self):
|
||||
# continue option
|
||||
p = Prompt()
|
||||
p.add_continue_option()
|
||||
self._check_default_option(p, Prompt.CONTINUE, Prompt.CONTINUE_DESCRIPTION)
|
||||
|
||||
# test add with description
|
||||
p = Prompt()
|
||||
p.add_continue_option("Other continue")
|
||||
self._check_default_option(p, Prompt.CONTINUE, "Other continue")
|
||||
|
||||
# change existing description
|
||||
p.add_continue_option("New continue")
|
||||
self._check_default_option(p, Prompt.CONTINUE, "New continue")
|
||||
|
||||
def test_quit_option(self):
|
||||
# quit option
|
||||
p = Prompt()
|
||||
p.add_quit_option()
|
||||
self._check_default_option(p, Prompt.QUIT, Prompt.QUIT_DESCRIPTION)
|
||||
|
||||
# test add with description
|
||||
p = Prompt()
|
||||
p.add_quit_option("Other quit")
|
||||
self._check_default_option(p, Prompt.QUIT, "Other quit")
|
||||
|
||||
# change existing description
|
||||
p.add_quit_option("New quit")
|
||||
self._check_default_option(p, Prompt.QUIT, "New quit")
|
||||
|
||||
def test_help_option(self):
|
||||
# help option
|
||||
p = Prompt()
|
||||
p.add_help_option()
|
||||
self._check_default_option(p, Prompt.HELP, Prompt.HELP_DESCRIPTION)
|
||||
|
||||
# test add with description
|
||||
p = Prompt()
|
||||
p.add_help_option("Other help")
|
||||
self._check_default_option(p, Prompt.HELP, "Other help")
|
||||
|
||||
# change existing description
|
||||
p.add_help_option("New help")
|
||||
self._check_default_option(p, Prompt.HELP, "New help")
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
# Rendering screen test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
from io import StringIO
|
||||
from unittest import mock
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render import RenderUnexpectedError
|
||||
from simpleline.render.screen import UIScreen, InputState
|
||||
|
||||
from .. import UtilityMixin
|
||||
|
||||
|
||||
def _fake_input(queue_instance, prompt):
|
||||
queue_instance.put("a")
|
||||
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=StringIO)
|
||||
class SeparatorPrinting_TestCase(unittest.TestCase, UtilityMixin):
|
||||
|
||||
def setUp(self):
|
||||
App.initialize()
|
||||
|
||||
def test_separator(self, stdout_mock):
|
||||
ui_screen = EmptyScreen()
|
||||
|
||||
self.schedule_screen_and_run(ui_screen)
|
||||
|
||||
self.assertEqual(self.calculate_separator(), stdout_mock.getvalue())
|
||||
|
||||
def test_other_width_separator(self, stdout_mock):
|
||||
ui_screen = EmptyScreen()
|
||||
width = 60
|
||||
|
||||
App.get_configuration().width = width
|
||||
App.get_scheduler().schedule_screen(ui_screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(self.calculate_separator(width), stdout_mock.getvalue())
|
||||
|
||||
def test_zero_width_no_separator(self, stdout_mock):
|
||||
ui_screen = EmptyScreen()
|
||||
width = 0
|
||||
|
||||
App.get_configuration().width = width
|
||||
App.get_scheduler().schedule_screen(ui_screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual("\n\n", stdout_mock.getvalue())
|
||||
|
||||
def test_no_separator_when_screen_setup_fails(self, stdout_mock):
|
||||
ui_screen = TestScreenSetupFail()
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(ui_screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual("", stdout_mock.getvalue())
|
||||
|
||||
def test_no_separator(self, stdout_mock):
|
||||
print_text = "testing"
|
||||
screen = NoSeparatorScreen(print_text)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
expected_output = print_text + "\n" + print_text + "\n"
|
||||
|
||||
self.assertEqual(expected_output, stdout_mock.getvalue())
|
||||
|
||||
|
||||
class SimpleUIScreenFeatures_TestCase(unittest.TestCase):
|
||||
|
||||
def test_close_screen(self):
|
||||
screen = UIScreen()
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
# Program will quit in close_screen when stack is empty
|
||||
App.get_scheduler().schedule_screen(UIScreen())
|
||||
screen.close()
|
||||
|
||||
def test_close_screen_closed_from_other_source_error(self):
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(UIScreen())
|
||||
with self.assertRaises(RenderUnexpectedError):
|
||||
App.get_scheduler().close_screen(closed_from=mock.MagicMock())
|
||||
|
||||
def test_failed_screen_setup(self):
|
||||
screen = FailedSetupScreen()
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=StringIO)
|
||||
class SimpleUIScreenProcessing_TestCase(unittest.TestCase, UtilityMixin):
|
||||
|
||||
def setUp(self):
|
||||
self._default_separator = self.calculate_separator(80)
|
||||
|
||||
def test_screen_event_loop_processing(self, _):
|
||||
ui_screen = EmptyScreen()
|
||||
|
||||
self.schedule_screen_and_run(ui_screen)
|
||||
|
||||
self.assertTrue(ui_screen.is_closed)
|
||||
|
||||
def test_running_empty_loop(self, _):
|
||||
App.initialize()
|
||||
loop = App.get_event_loop()
|
||||
loop.process_signals()
|
||||
|
||||
def test_screen_event_loop_processing_with_two_screens(self, _):
|
||||
first_screen = EmptyScreen()
|
||||
screen = EmptyScreen()
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(first_screen)
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(first_screen)
|
||||
self.assertTrue(screen)
|
||||
|
||||
def test_screen_title_rendering(self, stdout_mock):
|
||||
screen = NoInputScreen()
|
||||
screen.title = u"TestTitle"
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
out = self._default_separator
|
||||
out += "TestTitle\n\n"
|
||||
self.assertEqual(stdout_mock.getvalue(), out)
|
||||
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=StringIO)
|
||||
@mock.patch('simpleline.event_loop.AbstractEventLoop.kill_app_with_traceback')
|
||||
class ScreenException_TestCase(unittest.TestCase, UtilityMixin):
|
||||
|
||||
def setUp(self):
|
||||
self._force_quit_called = False
|
||||
|
||||
# The original method calls sys.exit(1) so we don't need to test this functionality
|
||||
def force_quit_mock(self, signal, data=None):
|
||||
self._force_quit_called = True
|
||||
loop = App.get_event_loop()
|
||||
loop.force_quit()
|
||||
|
||||
def test_raise_exception_in_refresh(self, mock_kill_app, _):
|
||||
screen = ExceptionTestScreen(ExceptionTestScreen.REFRESH)
|
||||
mock_kill_app.side_effect = self.force_quit_mock
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
self.assertTrue(self._force_quit_called)
|
||||
|
||||
def test_raise_exception_in_rendering(self, mock_kill_app, _):
|
||||
screen = ExceptionTestScreen(ExceptionTestScreen.REDRAW)
|
||||
mock_kill_app.side_effect = self.force_quit_mock
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
self.assertTrue(self._force_quit_called)
|
||||
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=StringIO)
|
||||
@mock.patch('simpleline.input.input_handler.InputHandlerRequest._get_input')
|
||||
class InputProcessing_TestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
App.initialize()
|
||||
|
||||
def test_basic_input(self, input_mock, mock_stdout):
|
||||
input_mock.return_value = "a"
|
||||
screen = InputScreen()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(screen.input_processed)
|
||||
|
||||
def test_process_input_and_redraw(self, input_mock, mock_stdout):
|
||||
input_mock.return_value = "a"
|
||||
screen = InputStateRedrawScreen()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(screen.refreshed)
|
||||
|
||||
def test_process_input_and_close(self, input_mock, mock_stdout):
|
||||
input_mock.return_value = "a"
|
||||
screen = InputStateCloseScreen()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(screen.input_processed)
|
||||
|
||||
def test_quit_input(self, mock_stdin, mock_stdout):
|
||||
mock_stdin.return_value = "q"
|
||||
screen = UIScreen()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
def test_continue_input(self, mock_stdin, mock_stdout):
|
||||
mock_stdin.return_value = "c"
|
||||
screen = UIScreen()
|
||||
screen2 = EmptyScreen()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.get_scheduler().schedule_screen(screen2)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(screen.screen_ready)
|
||||
self.assertTrue(screen.screen_ready)
|
||||
|
||||
def test_refresh_input(self, mock_stdin, mock_stdout):
|
||||
mock_stdin.return_value = "r"
|
||||
screen = RefreshTestScreen()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(screen.input_processed)
|
||||
|
||||
def test_refresh_on_input_error(self, mock_stdin, mock_stdout):
|
||||
mock_stdin.return_value = "q"
|
||||
threshold = 5
|
||||
screen = InputErrorTestScreen(threshold)
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(screen.render_counter, 2)
|
||||
self.assertEqual(screen.error_counter, threshold)
|
||||
|
||||
def test_multiple_refresh_on_input_error(self, mock_stdin, mock_stdout):
|
||||
mock_stdin.return_value = "q"
|
||||
threshold = 12
|
||||
screen = InputErrorTestScreen(threshold)
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(screen.render_counter, 3)
|
||||
self.assertEqual(screen.error_counter, threshold)
|
||||
|
||||
def test_no_refresh_when_prompt_is_none(self, mock_stdin, mock_stdout):
|
||||
mock_stdin.return_value = "q"
|
||||
threshold = 5
|
||||
screen = InputErrorDynamicPromptTestScreen(threshold, not_return_prompt_on=3)
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
# first draw and manual redraw when prompt is None
|
||||
self.assertEqual(screen.render_counter, 2)
|
||||
self.assertTrue(screen.input_skipped)
|
||||
self.assertEqual(screen.error_counter, threshold)
|
||||
|
||||
def test_input_no_prompt(self, mock_stdin, mock_stdout):
|
||||
screen = InputWithNoPrompt()
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(screen.prompt_entered)
|
||||
|
||||
@mock.patch('simpleline.event_loop.main_loop.MainLoop.process_signals')
|
||||
def test_custom_getpass(self, mock_stdin, mock_stdout, process_signals):
|
||||
prompt = mock.MagicMock()
|
||||
ret = "test"
|
||||
screen = TestScreenWithPassFunc(prompt, ret)
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(screen.pass_called)
|
||||
self.assertEqual(screen.pass_prompt.rstrip(), str(prompt))
|
||||
|
||||
def test_blocking_input(self, mock_stdin, mock_stdout):
|
||||
prompt_message = "test prompt"
|
||||
ret = "blocking test"
|
||||
mock_stdin.return_value = ret
|
||||
screen = BlockingInputTestScreen(prompt_message, False)
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(screen.input_returned, ret)
|
||||
|
||||
out = mock_stdout.getvalue()
|
||||
out = out.split("\n")[-1].strip()
|
||||
self.assertEqual(out, prompt_message)
|
||||
|
||||
@mock.patch('simpleline.global_configuration.GlobalConfiguration.password_function')
|
||||
def test_blocking_password_input(self, mock_getpass, mock_stdin, mock_stdout):
|
||||
prompt_message = "test prompt"
|
||||
ret = "blocking test"
|
||||
mock_getpass.return_value = ret
|
||||
screen = BlockingInputTestScreen(prompt_message, True)
|
||||
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(screen.input_returned, ret)
|
||||
|
||||
out = mock_stdout.getvalue()
|
||||
# can't check for prompt because that is printed by getpass func which is mocked
|
||||
self.assertGreater(len(out), 1)
|
||||
|
||||
|
||||
# HELPER CLASSES
|
||||
|
||||
class EmptyScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.is_closed = False
|
||||
EmptyScreen.title = ""
|
||||
self.input_required = False
|
||||
|
||||
def show_all(self):
|
||||
self.close()
|
||||
|
||||
def closed(self):
|
||||
self.is_closed = True
|
||||
|
||||
|
||||
class TestScreenSetupFail(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input_required = False
|
||||
|
||||
def setup(self, args):
|
||||
super().setup(args)
|
||||
return False
|
||||
|
||||
|
||||
class TestScreenWithPassFunc(UIScreen):
|
||||
|
||||
def __init__(self, prompt, return_value):
|
||||
super().__init__()
|
||||
self.pass_prompt = ""
|
||||
self.pass_called = False
|
||||
self.password_func = self._test_getpass
|
||||
self.hide_user_input = True
|
||||
self._prompt = prompt
|
||||
self._return_value = return_value
|
||||
|
||||
def prompt(self, args=None):
|
||||
return self._prompt
|
||||
|
||||
def _test_getpass(self, prompt):
|
||||
self.pass_prompt = prompt
|
||||
self.pass_called = True
|
||||
return self._return_value
|
||||
|
||||
def input(self, args, key):
|
||||
return InputState.PROCESSED_AND_CLOSE
|
||||
|
||||
|
||||
class InputErrorTestScreen(UIScreen):
|
||||
|
||||
def __init__(self, error_threshold=5):
|
||||
super().__init__()
|
||||
self.error_counter = 0
|
||||
self.render_counter = 0
|
||||
self._error_threshold = error_threshold
|
||||
|
||||
def input(self, args, key):
|
||||
if self.error_counter == self._error_threshold:
|
||||
# let "q" propagate to quit
|
||||
return key
|
||||
|
||||
self.error_counter += 1
|
||||
return InputState.DISCARDED
|
||||
|
||||
def show_all(self):
|
||||
self.render_counter += 1
|
||||
|
||||
|
||||
class InputErrorDynamicPromptTestScreen(InputErrorTestScreen):
|
||||
|
||||
def __init__(self, error_threshold=5, not_return_prompt_on=2):
|
||||
super().__init__(error_threshold=error_threshold)
|
||||
self._not_return_prompt_on = not_return_prompt_on
|
||||
self.input_skipped = False
|
||||
|
||||
def prompt(self, args=None):
|
||||
if self.error_counter == self._not_return_prompt_on and not self.input_skipped:
|
||||
self.input_skipped = True
|
||||
self.redraw()
|
||||
return None
|
||||
|
||||
return super().prompt(args)
|
||||
|
||||
|
||||
class InputWithNoPrompt(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.prompt_entered = False
|
||||
self.input_required = True
|
||||
|
||||
def prompt(self, args=None):
|
||||
self.prompt_entered = True
|
||||
self.close()
|
||||
# do not process input - it was processed here by user
|
||||
return None
|
||||
|
||||
|
||||
class RefreshTestScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input_processed = False
|
||||
|
||||
def input(self, args, key):
|
||||
self.input_processed = True
|
||||
return key
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
if self.input_processed:
|
||||
self.close()
|
||||
self.input_required = False
|
||||
return
|
||||
|
||||
|
||||
class FailedSetupScreen(UIScreen):
|
||||
|
||||
def setup(self, args):
|
||||
super().setup(args)
|
||||
return False
|
||||
|
||||
|
||||
class NoSeparatorScreen(UIScreen):
|
||||
|
||||
def __init__(self, print_string):
|
||||
super().__init__()
|
||||
self.input_required = False
|
||||
self.no_separator = True
|
||||
self._print_string = print_string
|
||||
|
||||
def show_all(self):
|
||||
print(self._print_string)
|
||||
self.close()
|
||||
|
||||
|
||||
class NoInputScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input_required = False
|
||||
|
||||
def show_all(self):
|
||||
super().show_all()
|
||||
self.close()
|
||||
|
||||
|
||||
class InputScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input_processed = False
|
||||
|
||||
def input(self, args, key):
|
||||
if key == "a":
|
||||
self.input_processed = True
|
||||
self.close()
|
||||
return InputState.PROCESSED
|
||||
|
||||
|
||||
class InputStateCloseScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input_processed = False
|
||||
|
||||
def input(self, args, key):
|
||||
self.input_processed = not self.input_processed
|
||||
return InputState.PROCESSED_AND_CLOSE
|
||||
|
||||
|
||||
class InputStateRedrawScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._refreshing = False
|
||||
self.refreshed = False
|
||||
|
||||
def input(self, args, key):
|
||||
if not self._refreshing:
|
||||
self._refreshing = True
|
||||
return InputState.PROCESSED_AND_REDRAW
|
||||
|
||||
self.refreshed = True
|
||||
return InputState.PROCESSED_AND_CLOSE
|
||||
|
||||
|
||||
class ExceptionTestScreen(UIScreen):
|
||||
"""Raising an exception in some place of processing."""
|
||||
|
||||
REFRESH = 0
|
||||
REDRAW = 1
|
||||
|
||||
def __init__(self, where):
|
||||
super().__init__()
|
||||
self._where = where
|
||||
self.input_required = False
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh()
|
||||
if self._where == self.REFRESH:
|
||||
raise TestRefreshException("Refresh test exception happened!")
|
||||
|
||||
def show_all(self):
|
||||
super().show_all()
|
||||
if self._where == self.REDRAW:
|
||||
raise TestRedrawException("Redraw test exception happened!")
|
||||
|
||||
|
||||
class BlockingInputTestScreen(EmptyScreen):
|
||||
|
||||
def __init__(self, prompt_message, hidden):
|
||||
super().__init__()
|
||||
self._prompt_message = prompt_message
|
||||
self._hidden = hidden
|
||||
self.input_returned = None
|
||||
|
||||
def show_all(self):
|
||||
self.input_returned = self.get_user_input(self._prompt_message, self._hidden)
|
||||
super().show_all()
|
||||
|
||||
|
||||
class TestRefreshException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TestRedrawException(Exception):
|
||||
pass
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
# Screen scheduler test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from simpleline.event_loop.main_loop import MainLoop
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.screen_scheduler import ScreenScheduler
|
||||
from simpleline.render.screen_stack import ScreenStack, ScreenStackEmptyException
|
||||
|
||||
|
||||
class Scheduler_TestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.stack = None
|
||||
self.scheduler = None
|
||||
|
||||
def create_scheduler_with_stack(self):
|
||||
self.stack = ScreenStack()
|
||||
self.scheduler = ScreenScheduler(event_loop=mock.MagicMock(), scheduler_stack=self.stack)
|
||||
|
||||
def pop_last_item(self, remove=True):
|
||||
return self.stack.pop(remove)
|
||||
|
||||
def test_create_scheduler(self):
|
||||
scheduler = ScreenScheduler(MainLoop())
|
||||
self.assertTrue(isinstance(scheduler._screen_stack, ScreenStack)) # pylint: disable=protected-access
|
||||
|
||||
def test_scheduler_quit_screen(self):
|
||||
def test_callback():
|
||||
pass
|
||||
scheduler = ScreenScheduler(MainLoop())
|
||||
self.assertEqual(scheduler.quit_screen, None)
|
||||
scheduler.quit_screen = test_callback
|
||||
self.assertEqual(scheduler.quit_screen, test_callback)
|
||||
|
||||
def test_nothing_to_render(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
self.assertTrue(self.scheduler.nothing_to_render)
|
||||
self.assertTrue(self.stack.empty())
|
||||
|
||||
self.scheduler.schedule_screen(UIScreen())
|
||||
self.assertFalse(self.scheduler.nothing_to_render)
|
||||
self.assertFalse(self.stack.empty())
|
||||
|
||||
def test_schedule_screen(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
screen = UIScreen()
|
||||
self.scheduler.schedule_screen(screen)
|
||||
test_screen = self.pop_last_item(False)
|
||||
self.assertEqual(test_screen.ui_screen, screen)
|
||||
self.assertEqual(test_screen.args, None) # empty field - no arguments
|
||||
self.assertFalse(test_screen.execute_new_loop)
|
||||
|
||||
# Schedule another screen, new one will be added to the bottom of the stack
|
||||
new_screen = UIScreen()
|
||||
self.scheduler.schedule_screen(new_screen)
|
||||
# Here should still be the old screen
|
||||
self.assertEqual(self.pop_last_item().ui_screen, screen)
|
||||
# After removing the first we would find the second screen
|
||||
self.assertEqual(self.pop_last_item().ui_screen, new_screen)
|
||||
|
||||
def test_replace_screen_with_empty_stack(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
with self.assertRaises(ScreenStackEmptyException):
|
||||
self.scheduler.replace_screen(UIScreen())
|
||||
|
||||
def test_replace_screen(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
old_screen = UIScreen()
|
||||
screen = UIScreen()
|
||||
self.scheduler.schedule_screen(old_screen)
|
||||
self.scheduler.replace_screen(screen)
|
||||
self.assertEqual(self.pop_last_item(False).ui_screen, screen)
|
||||
|
||||
new_screen = UIScreen()
|
||||
self.scheduler.replace_screen(new_screen)
|
||||
self.assertEqual(self.pop_last_item().ui_screen, new_screen)
|
||||
# The old_screen was replaced so the stack is empty now
|
||||
self.assertTrue(self.stack.empty())
|
||||
|
||||
def test_replace_screen_with_args(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
old_screen = UIScreen()
|
||||
screen = UIScreen()
|
||||
self.scheduler.schedule_screen(old_screen)
|
||||
self.scheduler.replace_screen(screen, "test")
|
||||
test_screen = self.pop_last_item()
|
||||
self.assertEqual(test_screen.ui_screen, screen)
|
||||
self.assertEqual(test_screen.args, "test")
|
||||
# The old_screen was replaced so the stack is empty now
|
||||
self.assertTrue(self.stack.empty())
|
||||
|
||||
def test_switch_screen_with_empty_stack(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
screen = UIScreen()
|
||||
self.scheduler.push_screen(screen)
|
||||
self.assertEqual(self.pop_last_item().ui_screen, screen)
|
||||
|
||||
def test_switch_screen(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
screen = UIScreen()
|
||||
new_screen = UIScreen()
|
||||
|
||||
self.scheduler.schedule_screen(screen)
|
||||
self.scheduler.push_screen(new_screen)
|
||||
|
||||
test_screen = self.pop_last_item()
|
||||
self.assertEqual(test_screen.ui_screen, new_screen)
|
||||
self.assertEqual(test_screen.args, None)
|
||||
self.assertEqual(test_screen.execute_new_loop, False)
|
||||
|
||||
# We popped the new_screen so the old screen should stay here
|
||||
self.assertEqual(self.pop_last_item().ui_screen, screen)
|
||||
self.assertTrue(self.stack.empty())
|
||||
|
||||
def test_switch_screen_with_args(self):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
screen = UIScreen()
|
||||
self.scheduler.push_screen(screen, args="test")
|
||||
self.assertEqual(self.pop_last_item(False).ui_screen, screen)
|
||||
self.assertEqual(self.pop_last_item().args, "test")
|
||||
|
||||
@mock.patch('simpleline.render.screen_scheduler.ScreenScheduler._draw_screen')
|
||||
def test_switch_screen_modal_empty_stack(self, _):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
screen = UIScreen()
|
||||
self.scheduler.push_screen_modal(screen)
|
||||
self.assertEqual(self.pop_last_item().ui_screen, screen)
|
||||
|
||||
@mock.patch('simpleline.render.screen_scheduler.ScreenScheduler._draw_screen')
|
||||
def test_switch_screen_modal(self, _):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
screen = UIScreen()
|
||||
new_screen = UIScreen()
|
||||
self.scheduler.schedule_screen(screen)
|
||||
self.scheduler.push_screen_modal(new_screen)
|
||||
|
||||
test_screen = self.pop_last_item()
|
||||
self.assertEqual(test_screen.ui_screen, new_screen)
|
||||
self.assertEqual(test_screen.args, None)
|
||||
self.assertEqual(test_screen.execute_new_loop, True)
|
||||
|
||||
@mock.patch('simpleline.render.screen_scheduler.ScreenScheduler._draw_screen')
|
||||
def test_switch_screen_modal_with_args(self, _):
|
||||
self.create_scheduler_with_stack()
|
||||
|
||||
screen = UIScreen()
|
||||
self.scheduler.push_screen_modal(screen, args="test")
|
||||
self.assertEqual(self.pop_last_item(False).ui_screen, screen)
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
# Screen scheduling test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from io import StringIO
|
||||
from unittest import mock
|
||||
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.screen_handler import ScreenHandler
|
||||
|
||||
from .. import UtilityMixin
|
||||
|
||||
|
||||
@mock.patch('sys.stdout', new_callable=StringIO)
|
||||
class ScreenScheduler_TestCase(unittest.TestCase, UtilityMixin):
|
||||
|
||||
def test_replace_screen(self, _):
|
||||
replace_screen = ShowedCounterScreen()
|
||||
screen = ShowedCounterScreen(replace_screen=replace_screen)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertEqual(screen.counter, 1)
|
||||
self.assertEqual(replace_screen.counter, 1)
|
||||
|
||||
def test_switch_screen(self, _):
|
||||
switched_screen = ShowedCounterScreen()
|
||||
screen = ShowedCounterScreen(switched_screen)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertEqual(screen.counter, 2)
|
||||
self.assertEqual(switched_screen.counter, 1)
|
||||
|
||||
def test_switch_screen_modal_in_render(self, _):
|
||||
modal_screen = ModalTestScreen()
|
||||
screen = ModalTestScreen(modal_screen_render=modal_screen)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertEqual(screen.copied_modal_counter, ModalTestScreen.AFTER_MODAL_RENDER)
|
||||
self.assertEqual(modal_screen.copied_modal_counter, ModalTestScreen.BEFORE_MODAL_RENDER)
|
||||
|
||||
def test_switch_screen_modal_in_refresh(self, _):
|
||||
modal_screen = ModalTestScreen()
|
||||
screen = ModalTestScreen(modal_screen_refresh=modal_screen)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertEqual(screen.copied_modal_counter, ModalTestScreen.AFTER_MODAL_REFRESH)
|
||||
self.assertEqual(modal_screen.copied_modal_counter, ModalTestScreen.BEFORE_MODAL_REFRESH)
|
||||
|
||||
def test_switch_screen_modal_refresh_and_render(self, _):
|
||||
modal_refresh = ModalTestScreen()
|
||||
modal_render = ModalTestScreen()
|
||||
screen = ModalTestScreen(modal_screen_refresh=modal_refresh,
|
||||
modal_screen_render=modal_render)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertEqual(screen.copied_modal_counter, ModalTestScreen.AFTER_MODAL_RENDER)
|
||||
self.assertEqual(modal_refresh.copied_modal_counter, ModalTestScreen.BEFORE_MODAL_REFRESH)
|
||||
self.assertEqual(modal_render.copied_modal_counter, ModalTestScreen.BEFORE_MODAL_RENDER)
|
||||
|
||||
def test_switch_screen_modal_render_recursive(self, _):
|
||||
modal_render_inner = ModalTestScreen()
|
||||
modal_render_outer = ModalTestScreen(modal_screen_render=modal_render_inner)
|
||||
screen = ModalTestScreen(modal_screen_render=modal_render_outer)
|
||||
|
||||
self.schedule_screen_and_run(screen)
|
||||
|
||||
self.assertEqual(screen.copied_modal_counter, ModalTestScreen.AFTER_MODAL_RENDER)
|
||||
# outer modal screen has AFTER_MODAL_RENDER because it was set before by inner loop
|
||||
self.assertEqual(modal_render_outer.copied_modal_counter,
|
||||
ModalTestScreen.AFTER_MODAL_RENDER)
|
||||
self.assertEqual(modal_render_inner.copied_modal_counter,
|
||||
ModalTestScreen.BEFORE_MODAL_RENDER)
|
||||
|
||||
@mock.patch('simpleline.input.input_handler.InputHandlerRequest._get_input')
|
||||
def test_switch_screen_modal_input_order(self, mock_input, mock_stdout):
|
||||
modal_screen = InputAndDrawScreen("Modal")
|
||||
parent_screen = EmitDrawThenCreateModal(modal_screen, msg="Parent")
|
||||
mock_input.return_value = "c"
|
||||
expected = ["Modal", # modal needs to be printed first
|
||||
"Parent", # draw enqueued draw signal -- manually registered in refresh()
|
||||
"Parent"] # draw because modal screen was closed
|
||||
|
||||
self.schedule_screen_and_run(parent_screen)
|
||||
|
||||
self.maxDiff = None
|
||||
self.assertEqual(self.create_output_with_separators(expected), mock_stdout.getvalue())
|
||||
|
||||
|
||||
class ShowedCounterScreen(UIScreen):
|
||||
|
||||
def __init__(self, switch_to_screen=None, replace_screen=None):
|
||||
super().__init__()
|
||||
self._switch_to_screen = switch_to_screen
|
||||
self._replace_screen = replace_screen
|
||||
self.counter = 0
|
||||
self.input_required = False
|
||||
|
||||
def show_all(self):
|
||||
super().show_all()
|
||||
self.counter += 1
|
||||
if self._switch_to_screen is not None:
|
||||
ScreenHandler.push_screen(self._switch_to_screen)
|
||||
self._switch_to_screen = None
|
||||
elif self._replace_screen is not None:
|
||||
ScreenHandler.replace_screen(self._replace_screen)
|
||||
self._replace_screen = None
|
||||
else:
|
||||
self.close()
|
||||
|
||||
|
||||
class ModalTestScreen(UIScreen):
|
||||
"""Test if the modal screen is really modal and stops the execution in a place where
|
||||
we start the modal screen.
|
||||
|
||||
This class have checkpoints which increment class variable counter. This counter is
|
||||
copied in the modal instance to the local variable self.copied_modal_counter.
|
||||
In the end we should check if the instance modal counter have the correct value, which is
|
||||
before the modal screen was started (1).
|
||||
"""
|
||||
|
||||
INIT = 0
|
||||
BEFORE_MODAL_REFRESH = 1
|
||||
AFTER_MODAL_REFRESH = 2
|
||||
BEFORE_MODAL_RENDER = 3
|
||||
AFTER_MODAL_RENDER = 4
|
||||
|
||||
modal_counter = 0
|
||||
|
||||
def __init__(self, modal_screen_render=None, modal_screen_refresh=None):
|
||||
super().__init__()
|
||||
self._modal_screen_render = modal_screen_render
|
||||
self._modal_screen_refresh = modal_screen_refresh
|
||||
self.copied_modal_counter = 0
|
||||
self.input_required = False
|
||||
ModalTestScreen.modal_counter = self.INIT
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
if self._modal_screen_refresh is not None:
|
||||
# Start a new modal screen
|
||||
ModalTestScreen.modal_counter = self.BEFORE_MODAL_REFRESH
|
||||
ScreenHandler.push_screen_modal(self._modal_screen_refresh)
|
||||
ModalTestScreen.modal_counter = self.AFTER_MODAL_REFRESH
|
||||
|
||||
def show_all(self):
|
||||
super().show_all()
|
||||
if self._modal_screen_render is not None:
|
||||
# Start new modal screen
|
||||
ModalTestScreen.modal_counter = self.BEFORE_MODAL_RENDER
|
||||
ScreenHandler.push_screen_modal(self._modal_screen_render)
|
||||
ModalTestScreen.modal_counter = self.AFTER_MODAL_RENDER
|
||||
|
||||
self.copied_modal_counter = ModalTestScreen.modal_counter
|
||||
self.close()
|
||||
|
||||
|
||||
class EmitDrawThenCreateModal(UIScreen):
|
||||
|
||||
def __init__(self, refresh_screen, msg):
|
||||
super().__init__()
|
||||
self._refresh_screen = refresh_screen
|
||||
self.title = msg
|
||||
self.input_required = False
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
if self._refresh_screen:
|
||||
self.redraw()
|
||||
ScreenHandler.push_screen_modal(self._refresh_screen)
|
||||
self._refresh_screen = None
|
||||
else:
|
||||
self.close()
|
||||
|
||||
|
||||
class InputAndDrawScreen(UIScreen):
|
||||
|
||||
def __init__(self, msg):
|
||||
super().__init__()
|
||||
self.title = msg
|
||||
self.input_required = False
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
self.close()
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
# Screen stack test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.screen_stack import ScreenStack, ScreenData, ScreenStackEmptyException
|
||||
|
||||
|
||||
class ScreenStack_TestCase(unittest.TestCase):
|
||||
|
||||
def test_append_screen(self):
|
||||
stack = ScreenStack()
|
||||
stack.append(ScreenData(None))
|
||||
|
||||
def test_is_empty(self):
|
||||
stack = ScreenStack()
|
||||
self.assertTrue(stack.empty())
|
||||
stack.append(ScreenData(None))
|
||||
self.assertFalse(stack.empty())
|
||||
|
||||
def test_pop(self):
|
||||
stack = ScreenStack()
|
||||
with self.assertRaises(ScreenStackEmptyException):
|
||||
stack.pop()
|
||||
|
||||
with self.assertRaises(ScreenStackEmptyException):
|
||||
stack.pop(False)
|
||||
|
||||
# stack.pop(True) will remove the item
|
||||
stack.append(ScreenData(None))
|
||||
stack.pop(True)
|
||||
with self.assertRaises(ScreenStackEmptyException):
|
||||
stack.pop()
|
||||
|
||||
# stack.pop() should behave the same as stack.pop(True)
|
||||
stack.append(ScreenData(None))
|
||||
stack.pop()
|
||||
with self.assertRaises(ScreenStackEmptyException):
|
||||
stack.pop()
|
||||
|
||||
stack.append(ScreenData(None))
|
||||
stack.pop(False)
|
||||
stack.pop(True)
|
||||
|
||||
def test_add_first(self):
|
||||
stack = ScreenStack()
|
||||
|
||||
screen_data = ScreenData(None)
|
||||
stack.add_first(screen_data)
|
||||
self.assertEqual(stack.pop(False), screen_data)
|
||||
|
||||
# Add new Screen data to the end
|
||||
new_screen_data = ScreenData(None)
|
||||
stack.add_first(new_screen_data)
|
||||
# First the old screen data should be there
|
||||
self.assertEqual(stack.pop(), screen_data)
|
||||
# Second should be the new screen data
|
||||
self.assertEqual(stack.pop(), new_screen_data)
|
||||
|
||||
def test_size(self):
|
||||
stack = ScreenStack()
|
||||
self.assertEqual(stack.size(), 0)
|
||||
|
||||
stack.append(ScreenData(None))
|
||||
self.assertEqual(stack.size(), 1)
|
||||
|
||||
stack.append(ScreenData(None))
|
||||
self.assertEqual(stack.size(), 2)
|
||||
|
||||
# Remove from stack
|
||||
stack.pop()
|
||||
self.assertEqual(stack.size(), 1)
|
||||
stack.pop()
|
||||
self.assertEqual(stack.size(), 0)
|
||||
|
||||
# Add first when stack has items
|
||||
stack.append(ScreenData(None))
|
||||
stack.append(ScreenData(None))
|
||||
self.assertEqual(stack.size(), 2)
|
||||
stack.add_first(ScreenData(None))
|
||||
self.assertEqual(stack.size(), 3)
|
||||
|
||||
def test_stack_dump(self):
|
||||
stack = ScreenStack()
|
||||
|
||||
stack.append(ScreenData(TestScreen1()))
|
||||
stack.append(ScreenData(TestScreen2()))
|
||||
|
||||
dump = stack.dump_stack()
|
||||
dump = dump.replace('\n', '')
|
||||
self.assertRegex(dump, r"TestScreen2.*TestScreen1")
|
||||
|
||||
|
||||
class ScreenData_TestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.ui_screen = None
|
||||
|
||||
def _prepare(self):
|
||||
self.ui_screen = UIScreen()
|
||||
|
||||
def _screen_check(self, test_screen, ui_screen, args, execute_new_loop):
|
||||
self._prepare()
|
||||
self.assertEqual(test_screen.ui_screen, ui_screen)
|
||||
self.assertEqual(test_screen.args, args)
|
||||
self.assertEqual(test_screen.execute_new_loop, execute_new_loop)
|
||||
|
||||
def test_screen_data(self):
|
||||
self._prepare()
|
||||
screen = ScreenData(self.ui_screen)
|
||||
self._screen_check(screen, self.ui_screen, None, False)
|
||||
|
||||
def test_screen_data_with_args(self):
|
||||
self._prepare()
|
||||
screen = ScreenData(ui_screen=self.ui_screen, args=1)
|
||||
self._screen_check(screen, self.ui_screen, 1, False)
|
||||
|
||||
array = [2, "a"]
|
||||
screen2 = ScreenData(ui_screen=self.ui_screen, args=array)
|
||||
self._screen_check(screen2, self.ui_screen, array, False)
|
||||
|
||||
def test_screen_data_with_execute_loop(self):
|
||||
self._prepare()
|
||||
screen = ScreenData(self.ui_screen, execute_new_loop=True)
|
||||
self._screen_check(screen, self.ui_screen, None, True)
|
||||
|
||||
screen2 = ScreenData(self.ui_screen, execute_new_loop=False)
|
||||
self._screen_check(screen2, self.ui_screen, None, False)
|
||||
|
||||
def test_screen_data_with_args_and_execute_loop(self):
|
||||
self._prepare()
|
||||
screen = ScreenData(self.ui_screen, "test", True)
|
||||
self._screen_check(screen, self.ui_screen, "test", True)
|
||||
|
||||
|
||||
class TestScreen1(UIScreen):
|
||||
pass
|
||||
|
||||
|
||||
class TestScreen2(UIScreen):
|
||||
pass
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# Signal handler test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.event_loop.signals import RenderScreenSignal, AbstractSignal
|
||||
from simpleline.render.screen import UIScreen
|
||||
|
||||
|
||||
class SignalHandler_TestCase(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.callback_called = False
|
||||
self.priority = 0
|
||||
|
||||
def _callback(self, signal, data):
|
||||
self.callback_called = True
|
||||
signal.test_attribute = True
|
||||
|
||||
def test_basic_connect(self):
|
||||
connect_screen = UIScreen()
|
||||
|
||||
App.initialize(scheduler=MagicMock())
|
||||
connect_screen.connect(TestSignal, self._callback)
|
||||
App.get_event_loop().enqueue_signal(TestSignal(self))
|
||||
App.get_event_loop().process_signals()
|
||||
|
||||
self.assertTrue(self.callback_called)
|
||||
|
||||
def test_create_signal(self):
|
||||
connect_screen = UIScreen()
|
||||
|
||||
App.initialize(scheduler=MagicMock())
|
||||
signal = connect_screen.create_signal(TestSignal, priority=20)
|
||||
|
||||
self.assertEqual(signal.priority, 20)
|
||||
self.assertTrue(isinstance(signal, TestSignal))
|
||||
# source is set by create_signal
|
||||
self.assertEqual(signal.source, connect_screen)
|
||||
|
||||
def test_emit(self):
|
||||
connect_screen = UIScreen()
|
||||
|
||||
App.initialize(scheduler=MagicMock())
|
||||
connect_screen.connect(TestSignal, self._callback)
|
||||
connect_screen.emit(TestSignal(self))
|
||||
App.get_event_loop().process_signals()
|
||||
|
||||
self.assertTrue(self.callback_called)
|
||||
|
||||
@patch('sys.stdout')
|
||||
def test_connect_react_on_rendering(self, _):
|
||||
connect_test_screen = TestRenderConnectHandler()
|
||||
screen2 = EmptyScreen()
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(connect_test_screen)
|
||||
App.get_scheduler().schedule_screen(screen2)
|
||||
App.run()
|
||||
|
||||
self.assertTrue(connect_test_screen.callback_called)
|
||||
|
||||
|
||||
class TestRenderConnectHandler(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.callback_called = False
|
||||
self.input_required = False
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
self.connect(RenderScreenSignal, self._callback)
|
||||
self.close()
|
||||
|
||||
def _callback(self, signal, args):
|
||||
self.callback_called = True
|
||||
|
||||
|
||||
class EmptyScreen(UIScreen):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.input_required = False
|
||||
|
||||
def show_all(self):
|
||||
super().show_all()
|
||||
self.close()
|
||||
|
||||
|
||||
class TestSignal(AbstractSignal):
|
||||
pass
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
# Widgets test classes.
|
||||
#
|
||||
# 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 unittest import TestCase
|
||||
|
||||
from simpleline.event_loop.ticket_machine import TicketMachine
|
||||
|
||||
|
||||
class TicketMachine_TestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self._tickets = TicketMachine()
|
||||
|
||||
def test_take_ticket(self):
|
||||
line_id = 0
|
||||
t = self._tickets.take_ticket(line_id)
|
||||
self.assertEqual(t, 0)
|
||||
t2 = self._tickets.take_ticket(line_id)
|
||||
self.assertNotEqual(t, t2)
|
||||
|
||||
def test_check_ticket(self):
|
||||
line_id = 0
|
||||
t = self._tickets.take_ticket(line_id)
|
||||
|
||||
self.assertFalse(self._tickets.check_ticket(line_id, t))
|
||||
|
||||
self._tickets.mark_line_to_go(line_id)
|
||||
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t))
|
||||
|
||||
def test_mark_multiple_tickets(self):
|
||||
line_id = 0
|
||||
|
||||
t1 = self._tickets.take_ticket(line_id)
|
||||
t2 = self._tickets.take_ticket(line_id)
|
||||
t3 = self._tickets.take_ticket(line_id)
|
||||
t4 = self._tickets.take_ticket(line_id)
|
||||
|
||||
self._tickets.mark_line_to_go(line_id)
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t1))
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t2))
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t3))
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t4))
|
||||
|
||||
def test_mark_one_of_lines(self):
|
||||
line_id1 = "a"
|
||||
line_id2 = "b"
|
||||
|
||||
t1 = self._tickets.take_ticket(line_id1)
|
||||
t2 = self._tickets.take_ticket(line_id1)
|
||||
t3 = self._tickets.take_ticket(line_id2)
|
||||
t4 = self._tickets.take_ticket(line_id2)
|
||||
|
||||
self._tickets.mark_line_to_go(line_id1)
|
||||
|
||||
self.assertTrue(self._tickets.check_ticket(line_id1, t1))
|
||||
self.assertTrue(self._tickets.check_ticket(line_id1, t2))
|
||||
self.assertFalse(self._tickets.check_ticket(line_id2, t3))
|
||||
self.assertFalse(self._tickets.check_ticket(line_id2, t4))
|
||||
|
||||
def text_check_re_using(self):
|
||||
line_id = "a"
|
||||
|
||||
t1 = self._tickets.take_ticket(line_id)
|
||||
t2 = self._tickets.take_ticket(line_id)
|
||||
t3 = self._tickets.take_ticket(line_id)
|
||||
|
||||
self._tickets.mark_line_to_go(line_id)
|
||||
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t1))
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t2))
|
||||
|
||||
# it needs to be False when you check it again
|
||||
self.assertFalse(self._tickets.check_ticket(line_id, t1))
|
||||
self.assertFalse(self._tickets.check_ticket(line_id, t2))
|
||||
|
||||
# take new ticket and mark the line again
|
||||
|
||||
t4 = self._tickets.take_ticket(line_id)
|
||||
|
||||
self._tickets.mark_line_to_go(line_id)
|
||||
|
||||
# old checked tickets should be invalid now
|
||||
self.assertFalse(self._tickets.check_ticket(line_id, t1))
|
||||
self.assertFalse(self._tickets.check_ticket(line_id, t2))
|
||||
# old not checked ticket should work
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t3))
|
||||
# new tickets should work
|
||||
self.assertTrue(self._tickets.check_ticket(line_id, t4))
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
# Widgets test classes.
|
||||
#
|
||||
# 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 unittest
|
||||
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
||||
from simpleline import App
|
||||
from simpleline.render.prompt import Prompt
|
||||
from simpleline.render.screen import UIScreen
|
||||
from simpleline.render.widgets import TextWidget, SeparatorWidget, CheckboxWidget, CenterWidget, \
|
||||
ColumnWidget, EntryWidget
|
||||
|
||||
|
||||
class BaseWidgets_TestCase(unittest.TestCase):
|
||||
"""Base class containing helper functions."""
|
||||
def setUp(self):
|
||||
self.w1 = TextWidget(u"Můj krásný dlouhý text")
|
||||
self.w2 = TextWidget(u"Test")
|
||||
self.w3 = TextWidget(u"Test 2")
|
||||
self.w4 = TextWidget(u"Krásný dlouhý text podruhé")
|
||||
self.w5 = TextWidget(u"Test 3")
|
||||
self.w6 = TextWidget("The rescue environment will now attempt "
|
||||
"to find your Linux installation and mount it under "
|
||||
"the directory : bla. You can then make any changes "
|
||||
"required to your system. Choose '1' to proceed with "
|
||||
"this step.\nYou can choose to mount your file "
|
||||
"systems read-only instead of read-write by choosing "
|
||||
"'2'.\nIf for some reason this process does not work "
|
||||
"choose '3' to skip directly to a shell.\n\n")
|
||||
self.w7 = TextWidget("Wrapping toooooooooooooooooooooooooooooooooooooooooooo"
|
||||
"oooooooooooooooooooooooooooooooooooooooooooooooooooooo long word.")
|
||||
self.w8 = TextWidget("Text that would be wrapped exactly at the screen width should"
|
||||
" have special test. This one.")
|
||||
|
||||
def evaluate_result(self, test_result, expected_result):
|
||||
self.assertEqual(len(test_result), len(expected_result))
|
||||
for i in range(0, len(test_result)): # pylint: disable=consider-using-enumerate
|
||||
self.assertEqual(test_result[i], expected_result[i])
|
||||
|
||||
|
||||
class Widgets_TestCase(BaseWidgets_TestCase):
|
||||
|
||||
def test_separator_widget(self):
|
||||
w = SeparatorWidget()
|
||||
w.render(80)
|
||||
|
||||
res_lines = w.get_lines()
|
||||
|
||||
expected_result = [u""]
|
||||
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_separator_widget_multiline(self):
|
||||
w = SeparatorWidget(3)
|
||||
w.render(80)
|
||||
|
||||
res_lines = w.get_lines()
|
||||
|
||||
expected_result = [u"",
|
||||
u"",
|
||||
u""]
|
||||
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_column_widget(self):
|
||||
# Test column text
|
||||
c = ColumnWidget([(20, [self.w1, self.w2, self.w3]),
|
||||
(25, [self.w4, self.w5]),
|
||||
(15, [self.w1, self.w2, self.w3])], spacing=3)
|
||||
c.render(80)
|
||||
res_lines = c.get_lines()
|
||||
|
||||
expected_result = [u"Můj krásný dlouhý Krásný dlouhý text Můj krásný",
|
||||
u"text podruhé dlouhý text",
|
||||
u"Test Test 3 Test",
|
||||
u"Test 2 Test 2"]
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_column_wrapping(self):
|
||||
# Test column wrapping text
|
||||
c = ColumnWidget([(15, [self.w1, self.w2, self.w3]), (10, [self.w4, self.w5])], spacing=1)
|
||||
c.render(80)
|
||||
|
||||
expected_result = [u"Můj krásný Krásný",
|
||||
u"dlouhý text dlouhý",
|
||||
u"Test text",
|
||||
u"Test 2 podruhé",
|
||||
u" Test 3"]
|
||||
|
||||
res_lines = c.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_multiline_text(self):
|
||||
self.w6.render(80)
|
||||
expected_result = [
|
||||
"The rescue environment will now attempt to find your Linux installation and",
|
||||
"mount it under the directory : bla. You can then make any changes required to",
|
||||
"your system. Choose '1' to proceed with this step.",
|
||||
"You can choose to mount your file systems read-only instead of read-write by",
|
||||
"choosing '2'.",
|
||||
"If for some reason this process does not work choose '3' to skip directly to a",
|
||||
"shell.",
|
||||
"",
|
||||
""]
|
||||
res_lines = self.w6.get_lines()
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_wrapping(self):
|
||||
# wrap long text
|
||||
self.w7.render(80)
|
||||
expected_result = [
|
||||
"Wrapping toooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo",
|
||||
"oooooooooooooooooooooooooooo long word."]
|
||||
res_lines = self.w7.get_lines()
|
||||
|
||||
self.assertEqual(len(res_lines), len(expected_result))
|
||||
for i in range(0, len(res_lines)): # pylint: disable=consider-using-enumerate
|
||||
self.assertEqual(res_lines[i], expected_result[i])
|
||||
|
||||
self.w8.render(80)
|
||||
# pylint: disable=line-too-long
|
||||
expected_result = ["Text that would be wrapped exactly at the screen width should have special test.",
|
||||
"This one."]
|
||||
res_lines = self.w8.get_lines()
|
||||
|
||||
self.evaluate_result(res_lines, expected_result)
|
||||
|
||||
def test_checkbox(self):
|
||||
checkbox = CheckboxWidget(title="Test Title", text="Description")
|
||||
|
||||
checkbox.render(80)
|
||||
|
||||
expected_result = [u"[ ] Test Title",
|
||||
u" (Description)"]
|
||||
|
||||
self.evaluate_result(checkbox.get_lines(), expected_result)
|
||||
|
||||
def test_completed_checkbox(self):
|
||||
checkbox = CheckboxWidget(title="Title", text="Description", completed=True)
|
||||
|
||||
checkbox.render(80)
|
||||
|
||||
expected_result = [u"[x] Title",
|
||||
u" (Description)"]
|
||||
|
||||
self.evaluate_result(checkbox.get_lines(), expected_result)
|
||||
|
||||
def test_key_checkbox(self):
|
||||
checkbox = CheckboxWidget(key="o", title="Title", text="Description", completed=True)
|
||||
|
||||
checkbox.render(80)
|
||||
|
||||
expected_result = [u"[o] Title",
|
||||
u" (Description)"]
|
||||
|
||||
self.evaluate_result(checkbox.get_lines(), expected_result)
|
||||
|
||||
def test_empty_checkbox(self):
|
||||
checkbox = CheckboxWidget()
|
||||
|
||||
checkbox.render(80)
|
||||
|
||||
expected_result = [u"[ ]"]
|
||||
|
||||
self.evaluate_result(checkbox.get_lines(), expected_result)
|
||||
|
||||
def test_checkbox_wrapping(self):
|
||||
checkbox = CheckboxWidget(title="Title", text="Testing\nwrapping")
|
||||
|
||||
checkbox.render(80)
|
||||
|
||||
expected_result = [u"[ ] Title",
|
||||
u" (Testing",
|
||||
u" wrapping)"]
|
||||
|
||||
self.evaluate_result(checkbox.get_lines(), expected_result)
|
||||
|
||||
def test_center_widget(self):
|
||||
w = CenterWidget(self.w2)
|
||||
|
||||
w.render(10)
|
||||
|
||||
expected_result = [u" Test"]
|
||||
|
||||
self.evaluate_result(w.get_lines(), expected_result)
|
||||
|
||||
def test_entry_widget(self):
|
||||
title = "Title"
|
||||
value = "Value"
|
||||
w = EntryWidget(title=title, value=value)
|
||||
|
||||
w.render(30)
|
||||
|
||||
expected_result = [title,
|
||||
value]
|
||||
|
||||
self.evaluate_result(w.get_lines(), expected_result)
|
||||
|
||||
def test_entry_too_long(self):
|
||||
title = "Title too long"
|
||||
value = "Value also too long"
|
||||
w = EntryWidget(title=title, value=value)
|
||||
|
||||
w.render(10)
|
||||
|
||||
expected_result = [u"Title too",
|
||||
u"long",
|
||||
u"Value also",
|
||||
u"too long"]
|
||||
|
||||
self.evaluate_result(w.get_lines(), expected_result)
|
||||
|
||||
def test_entry_value_empty(self):
|
||||
title = "Title"
|
||||
w = EntryWidget(title=title)
|
||||
|
||||
w.render(20)
|
||||
|
||||
expected_result = [title]
|
||||
|
||||
self.evaluate_result(w.get_lines(), expected_result=expected_result)
|
||||
|
||||
|
||||
@patch('simpleline.input.input_handler.InputHandlerRequest._get_input')
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
class WidgetProcessing_TestCase(unittest.TestCase):
|
||||
|
||||
@staticmethod
|
||||
def _calculate_spacer():
|
||||
# this calculation is taken from scheduler for default width '80'
|
||||
return '\n'.join(2 * [80 * '='])
|
||||
|
||||
def _expected_output(self, text, widget_height=20):
|
||||
|
||||
# two lines are always added to the printed size
|
||||
prompt_height = 2
|
||||
real_widget_height = widget_height - prompt_height
|
||||
|
||||
lines = text.split('\n')
|
||||
|
||||
# add Press ENTER... to the text
|
||||
if len(lines) - 1 >= real_widget_height:
|
||||
lines.insert(real_widget_height, "\nPress %s to continue: \n" % Prompt.ENTER)
|
||||
|
||||
msg = self._calculate_spacer() + '\n'
|
||||
msg += "\n".join(lines)
|
||||
msg += "\n"
|
||||
return msg
|
||||
|
||||
def test_draw_simple_widget(self, out_mock, in_mock):
|
||||
widget_text = "Test"
|
||||
screen = ScreenWithWidget(widget_text)
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(self._expected_output(widget_text), out_mock.getvalue())
|
||||
|
||||
def test_widget_multiline(self, out_mock, in_mock):
|
||||
widget_text = "Testing output\n\n\nAgain..."
|
||||
screen = ScreenWithWidget(widget_text)
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(self._expected_output(widget_text), out_mock.getvalue())
|
||||
|
||||
def test_widget_too_high(self, out_mock, in_mock):
|
||||
in_mock.return_value = "\n"
|
||||
in_mock.side_effect = lambda: print('\n')
|
||||
|
||||
widget_text = ("Line\n"
|
||||
"Line2\n"
|
||||
"Line3\n"
|
||||
"Line4\n"
|
||||
"Line5")
|
||||
# Screen height take into account also 2 lines for prompt
|
||||
screen = ScreenWithWidget(widget_text, height=6)
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(self._expected_output(widget_text, widget_height=6), out_mock.getvalue())
|
||||
|
||||
def test_widget_is_exactly_height_to_print(self, out_mock, in_mock):
|
||||
widget_text = ("Line\n"
|
||||
"Line2\n"
|
||||
"Line3\n"
|
||||
"Line4")
|
||||
# Screen height take into account also 2 lines for prompt
|
||||
screen = ScreenWithWidget(widget_text, height=6)
|
||||
|
||||
App.initialize()
|
||||
App.get_scheduler().schedule_screen(screen)
|
||||
App.run()
|
||||
|
||||
self.assertEqual(self._expected_output(widget_text, widget_height=6), out_mock.getvalue())
|
||||
|
||||
|
||||
class ScreenWithWidget(UIScreen):
|
||||
|
||||
def __init__(self, msg, height=25):
|
||||
super().__init__(screen_height=height)
|
||||
self._msg = msg
|
||||
self.input_required = False
|
||||
|
||||
def refresh(self, args=None):
|
||||
super().refresh(args)
|
||||
self.window.add(TextWidget(self._msg))
|
||||
|
||||
def show_all(self):
|
||||
super().show_all()
|
||||
self.close()
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z $PYTHON ]; then
|
||||
PYTHON=python3
|
||||
fi
|
||||
|
||||
SCRIPT_DIR=$(dirname $0)
|
||||
|
||||
$PYTHON -m unittest discover -v -s $SCRIPT_DIR/../ -p '*_test.py'
|
||||
Reference in New Issue
Block a user