feat(Anaconda): Local Repo

This commit is contained in:
2026-06-13 02:06:34 +02:00
parent 05ae9c36c3
commit c412cd5d33
203 changed files with 23997 additions and 7 deletions
@@ -0,0 +1,898 @@
format = 2
pkgname = python-simpleline
pkgbase = python-simpleline
pkgver = 1.8-1
pkgarch = any
pkgbuild_sha256sum = 7502d64c9ab7b8658cf85b6919f95cec8bbdc1febca98f91b01de326450080ea
packager = Unknown Packager
builddate = 1781307804
builddir = /home/nocode/Files/FWS-ISO/pkgbuilds/python-simpleline
startdir = /home/nocode/Files/FWS-ISO/pkgbuilds/python-simpleline
buildtool = makepkg
buildtoolver = 7.1.0
buildenv = !distcc
buildenv = color
buildenv = !ccache
buildenv = check
buildenv = !sign
options = strip
options = docs
options = !libtool
options = !staticlibs
options = emptydirs
options = zipman
options = purge
options = debug
options = lto
installed = aardvark-dns-1.17.1-1-x86_64
installed = abseil-cpp-20260107.1-1-x86_64
installed = accountsservice-26.13.3-1-x86_64
installed = acl-2.3.2-2-x86_64
installed = adwaita-cursors-50.0-1-any
installed = adwaita-fonts-50.0-1-any
installed = adwaita-icon-theme-50.0-1-any
installed = adwaita-icon-theme-legacy-46.2-3-any
installed = alsa-card-profiles-1:1.6.6-1-x86_64
installed = alsa-lib-1.2.16-1-x86_64
installed = alsa-plugins-1:1.2.12-5-x86_64
installed = alsa-topology-conf-1.2.5.1-4-any
installed = alsa-ucm-conf-1.2.16-2-any
installed = aom-3.14.1-1-x86_64
installed = appstream-1.1.2-1-x86_64
installed = aquamarine-0.12.1-1-x86_64
installed = arch-install-scripts-31-1-any
installed = archiso-88-1-any
installed = archlinux-keyring-20260420-1-any
installed = argon2-20190702-6-x86_64
installed = at-spi2-core-2.60.4-1-x86_64
installed = atkmm-2.28.5-2-x86_64
installed = attr-2.5.2-2-x86_64
installed = audit-4.1.4-2-x86_64
installed = autoconf-2.73-1-any
installed = automake-1.18.1-1-any
installed = avahi-1:0.9rc4-1-x86_64
installed = base-3-3-any
installed = base-devel-1-2-any
installed = bash-5.3.15-1-x86_64
installed = bemoji-0.4.0-1-any
installed = binutils-2.46+r70+g155188ea10a7-1-x86_64
installed = bison-3.8.2-8-x86_64
installed = blas-3.12.1-2-x86_64
installed = bluez-libs-5.86-6-x86_64
installed = boost-libs-1.91.0-1-x86_64
installed = brightnessctl-0.5.1-3-x86_64
installed = brotli-1.2.0-1-x86_64
installed = bubblewrap-0.11.2-1-x86_64
installed = bzip2-1.0.8-6-x86_64
installed = c-ares-1.34.6-1-x86_64
installed = ca-certificates-20240618-1-any
installed = ca-certificates-mozilla-3.124-1-x86_64
installed = ca-certificates-utils-20240618-1-any
installed = cabextract-1.11-2-x86_64
installed = cairo-1.18.4-1-x86_64
installed = cairomm-1.14.6-1-x86_64
installed = cairomm-1.16-1.18.1-1-x86_64
installed = catatonit-0.2.1-2-x86_64
installed = cblas-3.12.1-2-x86_64
installed = cliphist-1:0.7.0-2-x86_64
installed = clucene-2.3.3.4-17-x86_64
installed = cmake-4.3.3-1-x86_64
installed = compiler-rt-22.1.6-1-x86_64
installed = composefs-1.0.8-1-x86_64
installed = conmon-1:2.2.1-1-x86_64
installed = containers-common-1:0.67.1-3-any
installed = coreutils-9.11-1-x86_64
installed = cpio-2.15-3-x86_64
installed = cppdap-1.58.0-3-x86_64
installed = cracklib-2.10.3-1-x86_64
installed = criu-4.2-3-x86_64
installed = crun-1.28-1-x86_64
installed = cryptsetup-2.8.6-1-x86_64
installed = curl-8.20.0-7-x86_64
installed = dav1d-1.5.3-1-x86_64
installed = db5.3-5.3.28-7-x86_64
installed = dbus-1.16.2-1-x86_64
installed = dbus-broker-37-3-x86_64
installed = dbus-broker-units-37-3-x86_64
installed = dbus-units-37-3-x86_64
installed = dconf-0.49.0-1-x86_64
installed = debugedit-5.3-1-x86_64
installed = default-cursors-3-1-any
installed = desktop-file-utils-0.28-1-x86_64
installed = device-mapper-2.03.41-1-x86_64
installed = diffutils-3.12-2-x86_64
installed = discord-1:1.0.142-1-x86_64
installed = dkms-3.4.1-1-any
installed = dos2unix-7.5.6-1-x86_64
installed = dosfstools-4.2-5-x86_64
installed = double-conversion-3.4.0-1-x86_64
installed = duktape-2.7.0-7-x86_64
installed = e2fsprogs-1.47.4-1-x86_64
installed = efibootmgr-18-4-x86_64
installed = efivar-39-2-x86_64
installed = egl-gbm-1.1.3-1-x86_64
installed = egl-wayland-4:1.1.21-1-x86_64
installed = egl-wayland2-1.0.1-1-x86_64
installed = egl-x11-1.0.5-1-x86_64
installed = eglexternalplatform-1.2.1-1-any
installed = electron41-41.7.2-1-x86_64
installed = elfutils-0.195-1-x86_64
installed = erofs-utils-1.9.1-1-x86_64
installed = exo-4.20.0-2-x86_64
installed = expat-2.8.1-1-x86_64
installed = extra-cmake-modules-6.26.0-1-any
installed = fakeroot-1:1.37.2-1-x86_64
installed = fcft-3.3.3-1-x86_64
installed = ffmpeg-2:8.1.1-2-x86_64
installed = fftw-3.3.11-1-x86_64
installed = file-5.48-1-x86_64
installed = filesystem-2025.10.12-1-any
installed = findutils-4.10.0-3-x86_64
installed = firefox-151.0.4-1-x86_64
installed = flac-1.5.0-1-x86_64
installed = flatpak-1:1.18.0-1-x86_64
installed = flex-2.6.4-6-x86_64
installed = fmt-12.1.0-2-x86_64
installed = fontconfig-2:2.18.1-1-x86_64
installed = freerdp-2:3.26.0-1-x86_64
installed = freetype2-2.14.3-1-x86_64
installed = fribidi-1.0.16-2-x86_64
installed = fuse-common-3.18.2-1-x86_64
installed = fuse2-2.9.9-5-x86_64
installed = fuse3-3.18.2-1-x86_64
installed = fuzzel-1.14.1-1-x86_64
installed = gawk-5.4.0-1-x86_64
installed = gc-8.2.12-1-x86_64
installed = gcc-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = gcc-libs-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = gcr-3.41.2-2-x86_64
installed = gcr-4-4.4.0.1-1-x86_64
installed = gdb-17.2-1-x86_64
installed = gdb-common-17.2-1-x86_64
installed = gdbm-1.26-2-x86_64
installed = gdk-pixbuf2-2.44.6-2-x86_64
installed = gettext-1.0-2-x86_64
installed = giflib-6.1.3-1-x86_64
installed = git-2.54.0-1-x86_64
installed = glib-networking-1:2.80.1-1-x86_64
installed = glib2-2.88.1-1-x86_64
installed = glibc-2.43+r22+g8362e8ce10b2-2-x86_64
installed = glibmm-2.66.8-2-x86_64
installed = glibmm-2.68-2.88.0-2-x86_64
installed = glslang-1:1.4.350.0-1-x86_64
installed = glycin-2.1.1-1-x86_64
installed = gmp-6.3.0-3-x86_64
installed = gnome-themes-extra-1:3.28-1-any
installed = gnulib-l10n-20241231-1-any
installed = gnupg-2.4.9-1-x86_64
installed = gnutls-3.8.13-2-x86_64
installed = go-2:1.26.4-1-x86_64
installed = gobject-introspection-1.86.0-2-x86_64
installed = gobject-introspection-runtime-1.86.0-2-x86_64
installed = gparted-1.8.1-2-x86_64
installed = gperftools-2.18.1-1-x86_64
installed = gpgme-2.1.0-1-x86_64
installed = gpgmepp-2.1.0-1-x86_64
installed = gpm-1.20.7.r38.ge82d1a6-6-x86_64
installed = gpsd-3.27.5-1-x86_64
installed = gptfdisk-1.0.10-2-x86_64
installed = graphene-1.10.8-2-x86_64
installed = graphite-1:1.3.15-1-x86_64
installed = grep-3.12-2-x86_64
installed = grim-1.5.0-2-x86_64
installed = groff-1.24.1-1-x86_64
installed = grub-2:2.14-1-x86_64
installed = gsettings-desktop-schemas-50.1-1-any
installed = gsettings-system-schemas-50.1-1-any
installed = gsm-1.0.24-1-x86_64
installed = gssdp-1.6.5-1-x86_64
installed = gst-plugins-bad-libs-1.28.3-1-x86_64
installed = gst-plugins-base-libs-1.28.3-1-x86_64
installed = gstreamer-1.28.3-1-x86_64
installed = gtest-1.17.0-2-x86_64
installed = gtk-layer-shell-0.10.1-1-x86_64
installed = gtk-update-icon-cache-1:4.22.4-1-x86_64
installed = gtk3-1:3.24.52-1-x86_64
installed = gtk4-1:4.22.4-1-x86_64
installed = gtkmm-4.0-4.22.0-2-x86_64
installed = gtkmm3-3.24.11-1-x86_64
installed = guile-3.0.11-1-x86_64
installed = gupnp-1:1.6.10-1-x86_64
installed = gupnp-igd-1.6.0-2-x86_64
installed = gzip-1.14-2-x86_64
installed = harfbuzz-14.2.1-1-x86_64
installed = harfbuzz-icu-14.2.1-1-x86_64
installed = hicolor-icon-theme-0.18-1-any
installed = hidapi-0.15.0-1-x86_64
installed = highway-1.4.0-1-x86_64
installed = hunspell-1.7.3-1-x86_64
installed = hwdata-0.408-1-any
installed = hwloc-2.13.0-1-x86_64
installed = hyphen-2.8.9-1-x86_64
installed = hyprcursor-0.1.13-6-x86_64
installed = hyprgraphics-0.5.1-2-x86_64
installed = hypridle-0.1.7-9-x86_64
installed = hyprland-0.55.4-1-x86_64
installed = hyprland-guiutils-0.2.1-6-x86_64
installed = hyprland-qt-support-0.1.0-12-x86_64
installed = hyprlang-0.6.8-4-x86_64
installed = hyprlock-0.9.5-4-x86_64
installed = hyprpaper-0.8.4-2-x86_64
installed = hyprpicker-0.4.7-1-x86_64
installed = hyprpolkitagent-0.1.3-7-x86_64
installed = hyprshot-1.3.0-4-any
installed = hyprtoolkit-0.5.4-2-x86_64
installed = hyprutils-0.13.1-1-x86_64
installed = hyprwayland-scanner-0.4.6-1-x86_64
installed = hyprwire-0.3.1-2-x86_64
installed = iana-etc-20260530-1-any
installed = icu-78.3-1-x86_64
installed = imagemagick-7.1.2.25-1-x86_64
installed = iniparser-4.2.6-2-x86_64
installed = iproute2-7.0.0-1-x86_64
installed = iptables-1:1.8.13-1-x86_64
installed = iputils-20250605-1-x86_64
installed = iso-codes-4.20.1-1-any
installed = jack2-1.9.22-2-x86_64
installed = jansson-2.15.0-1-x86_64
installed = jbigkit-2.1-8-x86_64
installed = jq-1.8.1-3-x86_64
installed = json-c-0.18-2-x86_64
installed = json-glib-1.10.8-1-x86_64
installed = jsoncpp-1.9.6-3-x86_64
installed = kbd-2.10.0-1-x86_64
installed = kcoreaddons-6.26.0-1-x86_64
installed = keyutils-1.6.3-4-x86_64
installed = ki18n-6.26.0-1-x86_64
installed = kitty-0.47.1-1-x86_64
installed = kitty-shell-integration-0.47.1-1-x86_64
installed = kitty-terminfo-0.47.1-1-x86_64
installed = kmod-34.2-1-x86_64
installed = kpmcore-26.04.2-1-x86_64
installed = krb5-1.22.2-1-x86_64
installed = kwidgetsaddons-6.26.0-1-x86_64
installed = l-smash-2.14.5-4-x86_64
installed = lame-3.101.r6531-1-x86_64
installed = lapack-3.12.1-2-x86_64
installed = lcms2-2.19.1-1-x86_64
installed = leancrypto-1.7.2-1-x86_64
installed = lib32-alsa-lib-1.2.16-1-x86_64
installed = lib32-alsa-plugins-1.2.12-1-x86_64
installed = lib32-brotli-1.1.0-1-x86_64
installed = lib32-bzip2-1.0.8-4-x86_64
installed = lib32-curl-8.20.0-7-x86_64
installed = lib32-dbus-1.16.2-1-x86_64
installed = lib32-e2fsprogs-1.47.4-1-x86_64
installed = lib32-expat-2.8.1-1-x86_64
installed = lib32-fontconfig-2:2.18.1-1-x86_64
installed = lib32-freetype2-2.14.3-1-x86_64
installed = lib32-gcc-libs-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = lib32-glib2-2.88.1-1-x86_64
installed = lib32-glibc-2.43+r22+g8362e8ce10b2-2-x86_64
installed = lib32-gmp-6.3.0-2-x86_64
installed = lib32-gnutls-3.8.13-3-x86_64
installed = lib32-icu-78.3-1-x86_64
installed = lib32-json-c-0.18-2-x86_64
installed = lib32-keyutils-1.6.3-4-x86_64
installed = lib32-krb5-1.22.2-1-x86_64
installed = lib32-libdrm-2.4.134-1-x86_64
installed = lib32-libelf-0.195-1-x86_64
installed = lib32-libffi-3.5.2-1-x86_64
installed = lib32-libgcrypt-1.12.2-1-x86_64
installed = lib32-libglvnd-1.7.0-1-x86_64
installed = lib32-libgpg-error-1.61-1-x86_64
installed = lib32-libidn2-2.3.8-1-x86_64
installed = lib32-libldap-2.6.13-1-x86_64
installed = lib32-libnghttp2-1.69.0-1-x86_64
installed = lib32-libnghttp3-1.16.0-1-x86_64
installed = lib32-libngtcp2-1.23.0-1-x86_64
installed = lib32-libnm-1.56.1-1-x86_64
installed = lib32-libpciaccess-0.19-1-x86_64
installed = lib32-libpipewire-1:1.6.6-1-x86_64
installed = lib32-libpng-1.6.58-1-x86_64
installed = lib32-libpsl-0.21.5-1-x86_64
installed = lib32-libssh2-1.11.1-1-x86_64
installed = lib32-libtasn1-4.21.0-1-x86_64
installed = lib32-libunistring-1.4.2-1-x86_64
installed = lib32-libva-2.22.0-1-x86_64
installed = lib32-libx11-1.8.13-1-x86_64
installed = lib32-libxau-1.0.12-1-x86_64
installed = lib32-libxcb-1.17.0-1-x86_64
installed = lib32-libxcrypt-4.5.2-1-x86_64
installed = lib32-libxcrypt-compat-4.5.2-1-x86_64
installed = lib32-libxdmcp-1.1.5-1-x86_64
installed = lib32-libxext-1.3.7-1-x86_64
installed = lib32-libxfixes-6.0.1-2-x86_64
installed = lib32-libxinerama-1.1.6-1-x86_64
installed = lib32-libxml2-2.15.3-1-x86_64
installed = lib32-libxshmfence-1.3.3-1-x86_64
installed = lib32-libxss-1.2.5-1-x86_64
installed = lib32-libxxf86vm-1.1.5-2-x86_64
installed = lib32-llvm-libs-1:22.1.6-1-x86_64
installed = lib32-lm_sensors-1:3.6.2-2-x86_64
installed = lib32-mesa-1:26.1.2-1-x86_64
installed = lib32-ncurses-6.6-2-x86_64
installed = lib32-nettle-4.0-2-x86_64
installed = lib32-nspr-4.39-1-x86_64
installed = lib32-nss-3.124-1-x86_64
installed = lib32-nvidia-utils-610.43.02-1-x86_64
installed = lib32-openssl-1:3.6.3-1-x86_64
installed = lib32-p11-kit-0.26.2-1-x86_64
installed = lib32-pcre2-10.47-1-x86_64
installed = lib32-pipewire-1:1.6.6-1-x86_64
installed = lib32-spirv-tools-1:1.4.350.0-1-x86_64
installed = lib32-sqlite-3.53.2-1-x86_64
installed = lib32-systemd-260.2-1-x86_64
installed = lib32-util-linux-2.42.1-1-x86_64
installed = lib32-vulkan-icd-loader-1.4.350.0-1-x86_64
installed = lib32-wayland-1.25.0-1-x86_64
installed = lib32-xz-5.8.3-1-x86_64
installed = lib32-zlib-1.3.2-1-x86_64
installed = lib32-zstd-1.5.7-2-x86_64
installed = libabw-0.1.4-1-x86_64
installed = libadwaita-1:1.9.1-1-x86_64
installed = libaio-0.3.113-4-x86_64
installed = libappindicator-12.10.1-1-x86_64
installed = libarchive-3.8.7-1-x86_64
installed = libasan-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libass-0.17.4-2-x86_64
installed = libassuan-3.0.0-1-x86_64
installed = libasyncns-1:0.8+r3+g68cd5af-3-x86_64
installed = libatomic-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libatomic_ops-7.8.2-2-x86_64
installed = libavc1394-0.5.4-7-x86_64
installed = libavif-1.4.2-1-x86_64
installed = libb2-0.98.1-3-x86_64
installed = libblake3-1.8.4-1-x86_64
installed = libbluray-1.4.1-1-x86_64
installed = libbpf-1.7.0-1-x86_64
installed = libbs2b-3.1.0-10-x86_64
installed = libbsd-0.12.2-2-x86_64
installed = libburn-1.5.8-1-x86_64
installed = libcanberra-1:0.30+r2+gc0620e4-6-x86_64
installed = libcap-2.78-1-x86_64
installed = libcap-ng-0.9.3-1-x86_64
installed = libcbor-0.14.0-1-x86_64
installed = libcdio-2.3.0-1-x86_64
installed = libcdio-paranoia-10.2+2.0.2-2-x86_64
installed = libcdr-0.1.9-1-x86_64
installed = libcloudproviders-0.4.0-1-x86_64
installed = libcmis-0.6.3-1-x86_64
installed = libcolord-1.4.8-1-x86_64
installed = libcups-2:2.4.19-1-x86_64
installed = libdaemon-0.14-6-x86_64
installed = libdatrie-0.2.14-1-x86_64
installed = libdbusmenu-glib-18.10.20180917-1-x86_64
installed = libdbusmenu-gtk3-18.10.20180917-1-x86_64
installed = libdecor-0.2.5-1-x86_64
installed = libdeflate-1.25-1-x86_64
installed = libdisplay-info-0.3.0-1-x86_64
installed = libdovi-3.3.2-1-x86_64
installed = libdrm-2.4.134-1-x86_64
installed = libdvdnav-7.0.0-1-x86_64
installed = libdvdread-7.0.1-1-x86_64
installed = libe-book-0.1.3-20-x86_64
installed = libebur128-1.2.6-2-x86_64
installed = libedit-20260512_3.1-1-x86_64
installed = libei-1.6.0-1-x86_64
installed = libelf-0.195-1-x86_64
installed = libepoxy-1.5.10-3-x86_64
installed = libepubgen-0.1.1-6-x86_64
installed = libetonyek-0.1.13-2-x86_64
installed = libevdev-1.13.6-1-x86_64
installed = libevent-2.1.12-5-x86_64
installed = libexif-0.6.26-1-x86_64
installed = libexttextcat-3.4.8-1-x86_64
installed = libfdk-aac-2.0.3-2-x86_64
installed = libffi-3.5.2-1-x86_64
installed = libfido2-1.17.0-1-x86_64
installed = libfontenc-1.1.9-1-x86_64
installed = libfreeaptx-0.2.2-1-x86_64
installed = libfreehand-0.1.3-1-x86_64
installed = libfyaml-0.9.6-2-x86_64
installed = libgcc-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libgcrypt-1.12.2-1-x86_64
installed = libgfortran-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libgirepository-1.86.0-2-x86_64
installed = libgit2-1:1.9.4-1-x86_64
installed = libglvnd-1.7.0-3-x86_64
installed = libgomp-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libgpg-error-1.61-1-x86_64
installed = libgtop-2.41.3-2-x86_64
installed = libgudev-238-3-x86_64
installed = libice-1.1.2-1-x86_64
installed = libidn2-2.3.8-1-x86_64
installed = libiec61883-1.2.0-9-x86_64
installed = libimobiledevice-1.4.0-2-x86_64
installed = libimobiledevice-glue-1.3.2-1-x86_64
installed = libinih-62-2-x86_64
installed = libinput-1.31.3-1-x86_64
installed = libisl-0.27-1-x86_64
installed = libisoburn-1.5.8.2-1-x86_64
installed = libisofs-1.5.8.2-1-x86_64
installed = libixion-0.20.0-7-x86_64
installed = libjpeg-turbo-3.1.4.1-1-x86_64
installed = libjxl-0.11.2-2-x86_64
installed = libksba-1.8.0-1-x86_64
installed = liblangtag-0.6.8-1-x86_64
installed = liblc3-1.1.3-2-x86_64
installed = libldac-2.0.2.3-3-x86_64
installed = libldap-2.6.13-1-x86_64
installed = liblqr-0.4.3-1-x86_64
installed = liblsan-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libmakepkg-dropins-20-1-any
installed = libmalcontent-0.14.0-4-x86_64
installed = libmd-1.2.0-1-x86_64
installed = libmm-glib-1.24.2-1-x86_64
installed = libmnl-1.0.5-2-x86_64
installed = libmodplug-0.8.9.0-7-x86_64
installed = libmpc-1.4.1-1-x86_64
installed = libmpdclient-2.23-1-x86_64
installed = libmspub-0.1.5-1-x86_64
installed = libmwaw-0.3.22-4-x86_64
installed = libmysofa-1.3.4-1-x86_64
installed = libndp-1.9-1-x86_64
installed = libnet-2:1.3-2-x86_64
installed = libnetfilter_conntrack-1.1.1-1-x86_64
installed = libnewt-0.52.25-2-x86_64
installed = libnfnetlink-1.0.2-2-x86_64
installed = libnftnl-1.3.1-1-x86_64
installed = libnghttp2-1.69.0-1-x86_64
installed = libnghttp3-1.16.0-1-x86_64
installed = libngtcp2-1.23.0-1-x86_64
installed = libnice-0.1.23-1-x86_64
installed = libnl-3.12.0-1-x86_64
installed = libnm-1.56.1-1-x86_64
installed = libnma-1.10.6-3-x86_64
installed = libnma-common-1.10.6-3-x86_64
installed = libnotify-0.8.8-1-x86_64
installed = libnsl-2.0.1-2-x86_64
installed = libnumbertext-1.0.11-3-x86_64
installed = libobjc-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libodfgen-0.1.8-5-x86_64
installed = libogg-1.3.6-1-x86_64
installed = libopenmpt-0.8.7-1-x86_64
installed = libopenrazer-0.4.0-1-x86_64
installed = liborcus-0.21.0-6-x86_64
installed = libp11-kit-0.26.2-1-x86_64
installed = libpagemaker-0.0.4-5-x86_64
installed = libpcap-1.10.6-1-x86_64
installed = libpciaccess-0.19-1-x86_64
installed = libpgm-5.3.128-4-x86_64
installed = libpipewire-1:1.6.6-1-x86_64
installed = libplacebo-7.360.1-2-x86_64
installed = libplist-2.7.0-3-x86_64
installed = libpng-1.6.58-1-x86_64
installed = libproxy-0.5.12-1-x86_64
installed = libpsl-0.21.5-2-x86_64
installed = libpulse-17.0+r98+gb096704c0-1-x86_64
installed = libpwquality-1.4.5-7-x86_64
installed = libquadmath-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libqxp-0.0.3-1-x86_64
installed = libraqm-0.10.5-1-x86_64
installed = libraw1394-2.1.2-4-x86_64
installed = libreoffice-fresh-26.2.4-1-x86_64
installed = librevenge-0.0.5-4-x86_64
installed = librsvg-2:2.62.3-1-x86_64
installed = librsync-1:2.3.4-2-x86_64
installed = libsamplerate-0.2.2-3-x86_64
installed = libsasl-2.1.28-5-x86_64
installed = libseccomp-2.6.0-1-x86_64
installed = libsecret-0.21.7-1-x86_64
installed = libsigc++-2.12.2-1-x86_64
installed = libsigc++-3.0-3.8.1-1-x86_64
installed = libsixel-1.10.5-1-x86_64
installed = libsm-1.2.6-1-x86_64
installed = libsndfile-1.2.2-4-x86_64
installed = libsodium-1.0.22-1-x86_64
installed = libsoup3-3.6.6-2-x86_64
installed = libsoxr-0.1.3-4-x86_64
installed = libssh-0.12.0-1-x86_64
installed = libssh2-1.11.1-1-x86_64
installed = libstaroffice-0.0.7-5-x86_64
installed = libstdc++-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libstemmer-3.1.1-1-x86_64
installed = libsysprof-capture-50.0-2-x86_64
installed = libtasn1-4.21.0-1-x86_64
installed = libtatsu-1.0.5-1-x86_64
installed = libteam-1.32-3-x86_64
installed = libthai-0.1.30-1-x86_64
installed = libtheora-1.2.0-1-x86_64
installed = libtiff-4.7.1-2-x86_64
installed = libtirpc-1.3.7-1-x86_64
installed = libtommath-1.3.0-2-x86_64
installed = libtool-2.6.1-1-x86_64
installed = libtsan-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libubsan-16.1.1+r12+g301eb08fa2c5-1-x86_64
installed = libunibreak-7.0-1-x86_64
installed = libunistring-1.4.2-1-x86_64
installed = libunwind-1.8.2-1-x86_64
installed = liburing-2.14-1-x86_64
installed = libusb-1.0.30-1-x86_64
installed = libusbmuxd-2.1.1-2-x86_64
installed = libutf8proc-2.11.3-1-x86_64
installed = libuv-1.52.1-1-x86_64
installed = libva-2.23.0-1-x86_64
installed = libva-nvidia-driver-0.0.17-1-x86_64
installed = libvdpau-1.5-4-x86_64
installed = libverto-0.3.2-6-x86_64
installed = libvisio-0.1.11-1-x86_64
installed = libvorbis-1.3.7-4-x86_64
installed = libvpl-2.16.0-2-x86_64
installed = libvpx-1.16.0-3-x86_64
installed = libwacom-2.19.0-1-x86_64
installed = libwebp-1.6.0-2-x86_64
installed = libwireplumber-0.5.14-1-x86_64
installed = libwpd-0.10.3-6-x86_64
installed = libwps-0.4.14-4-x86_64
installed = libx11-1.8.13-1-x86_64
installed = libxau-1.0.12-1-x86_64
installed = libxcb-1.17.0-1-x86_64
installed = libxcomposite-0.4.7-1-x86_64
installed = libxcrypt-4.5.2-1-x86_64
installed = libxcrypt-compat-4.5.2-1-x86_64
installed = libxcursor-1.2.3-1-x86_64
installed = libxcvt-0.1.3-1-x86_64
installed = libxdamage-1.1.7-1-x86_64
installed = libxdmcp-1.1.5-2-x86_64
installed = libxext-1.3.7-1-x86_64
installed = libxfce4ui-4.20.2-1-x86_64
installed = libxfce4util-4.20.1-1-x86_64
installed = libxfixes-6.0.2-1-x86_64
installed = libxfont2-2.0.7-1-x86_64
installed = libxft-2.3.9-1-x86_64
installed = libxi-1.8.3-1-x86_64
installed = libxinerama-1.1.6-1-x86_64
installed = libxkbcommon-1.13.2-1-x86_64
installed = libxkbcommon-x11-1.13.2-1-x86_64
installed = libxkbfile-1.2.0-1-x86_64
installed = libxml2-2.15.3-1-x86_64
installed = libxml2-legacy-2.13.9-2-x86_64
installed = libxmlb-0.3.27-1-x86_64
installed = libxmp-4.7.0-1-x86_64
installed = libxmu-1.3.1-1-x86_64
installed = libxpresent-1.0.2-1-x86_64
installed = libxrandr-1.5.5-1-x86_64
installed = libxrender-0.9.12-1-x86_64
installed = libxshmfence-1.3.3-1-x86_64
installed = libxslt-1.1.45-2-x86_64
installed = libxss-1.2.5-1-x86_64
installed = libxt-1.3.1-1-x86_64
installed = libxtst-1.2.5-1-x86_64
installed = libxv-1.0.13-1-x86_64
installed = libxxf86vm-1.1.7-1-x86_64
installed = libyaml-0.2.5-3-x86_64
installed = libyuv-r2426+464c51a03-1-x86_64
installed = libzip-1.11.4-1-x86_64
installed = libzmf-0.0.2-20-x86_64
installed = licenses-20240728-1-any
installed = lilv-0.26.4-1-x86_64
installed = linux-7.0.11.arch1-1-x86_64
installed = linux-api-headers-7.0-1-x86_64
installed = linux-firmware-20260519-1-any
installed = linux-firmware-amdgpu-20260519-1-any
installed = linux-firmware-atheros-20260519-1-any
installed = linux-firmware-broadcom-20260519-1-any
installed = linux-firmware-cirrus-20260519-1-any
installed = linux-firmware-intel-20260519-1-any
installed = linux-firmware-mediatek-20260519-1-any
installed = linux-firmware-nvidia-20260519-1-any
installed = linux-firmware-other-20260519-1-any
installed = linux-firmware-radeon-20260519-1-any
installed = linux-firmware-realtek-20260519-1-any
installed = linux-firmware-whence-20260519-1-any
installed = linux-headers-7.0.11.arch1-1-x86_64
installed = lld-22.1.6-1-x86_64
installed = llhttp-9.3.1-1-x86_64
installed = llvm-libs-22.1.6-1-x86_64
installed = lm_sensors-1:3.6.2-1-x86_64
installed = lmdb-0.9.35-1-x86_64
installed = lpsolve-5.5.2.14-1-x86_64
installed = lsb-release-2.0.r55.a25a4fc-1-any
installed = lsof-4.99.6-1-x86_64
installed = lua-5.5.0-2-x86_64
installed = lua54-5.4.8-6-x86_64
installed = luajit-2.1.1780076327+b925b3e-1-x86_64
installed = lug-helper-1:4.13-1-any
installed = lv2-1.18.10-2-x86_64
installed = lz4-1:1.10.0-2-x86_64
installed = lzo-2.10-5-x86_64
installed = m4-1.4.21-2-x86_64
installed = mailcap-2.1.54-2-any
installed = make-4.4.1-3-x86_64
installed = mako-1.11.0-1-x86_64
installed = md4c-0.5.3-1-x86_64
installed = mesa-1:26.1.2-1-x86_64
installed = meson-1.11.1-3-any
installed = mingw-w64-binutils-2.46.0-1-x86_64
installed = mingw-w64-crt-14.0.0-1-any
installed = mingw-w64-gcc-16.1.0-1-x86_64
installed = mingw-w64-headers-14.0.0-1-any
installed = mingw-w64-winpthreads-14.0.0-1-any
installed = minizip-1:1.3.2-3-x86_64
installed = mkinitcpio-41-4-any
installed = mkinitcpio-busybox-1.36.1-1-x86_64
installed = mobile-broadband-provider-info-20251101-1-any
installed = mpdecimal-4.0.1-3-x86_64
installed = mpfr-4.2.2-1-x86_64
installed = mpg123-1.33.5-1-x86_64
installed = mpv-1:0.41.0-3-x86_64
installed = mpvpaper-1.8-1-x86_64
installed = mtdev-1.1.7-1-x86_64
installed = mtools-1:4.0.49-1-x86_64
installed = mujs-1.3.9-1-x86_64
installed = muparser-2.3.5-2-x86_64
installed = nano-9.0-1-x86_64
installed = ncurses-6.6-2-x86_64
installed = neon-0.37.1-1-x86_64
installed = netavark-1.17.2-2-x86_64
installed = nettle-4.0-1-x86_64
installed = network-manager-applet-1.36.0-2-x86_64
installed = networkmanager-1.56.1-1-x86_64
installed = nftables-1:1.1.6-3-x86_64
installed = ninja-1.13.2-3-x86_64
installed = nm-connection-editor-1.36.0-2-x86_64
installed = noto-fonts-1:2026.06.01-1-any
installed = noto-fonts-cjk-20240730-1-any
installed = noto-fonts-emoji-1:2.051-1-any
installed = noto-fonts-extra-1:2026.06.01-1-any
installed = npth-1.8-1-x86_64
installed = nspr-4.39-1-x86_64
installed = nss-3.124-1-x86_64
installed = nvidia-open-610.43.02-2-x86_64
installed = nvidia-utils-610.43.02-2-x86_64
installed = ocl-icd-2.3.4-1-x86_64
installed = onetbb-2023.0.0-1-x86_64
installed = oniguruma-6.9.10-1-x86_64
installed = openal-1.25.2-1-x86_64
installed = opencore-amr-0.1.6-2-x86_64
installed = openjpeg2-2.5.4-1-x86_64
installed = openrazer-daemon-3.12.2-2-any
installed = openrazer-driver-dkms-3.12.2-2-any
installed = openssh-10.3p1-1-x86_64
installed = openssl-3.6.3-1-x86_64
installed = opus-1.6.1-1-x86_64
installed = opusfile-0.12-4-x86_64
installed = orc-0.4.42-1-x86_64
installed = os-prober-1.84-1-x86_64
installed = ostree-2025.7-3-x86_64
installed = p11-kit-0.26.2-1-x86_64
installed = pacman-7.1.0.r9.g54d9411-2-x86_64
installed = pacman-mirrorlist-20260610-1-any
installed = pahole-1:1.31-2-x86_64
installed = pam-1.7.2-2-x86_64
installed = pambase-20250719-1-any
installed = pango-1:1.57.1-1-x86_64
installed = pangomm-2.46.4-3-x86_64
installed = pangomm-2.48-2.56.1-2-x86_64
installed = papirus-icon-theme-20250501-1-any
installed = parted-3.7-1-x86_64
installed = paru-2.1.0-2-x86_64
installed = paru-debug-2.1.0-2-x86_64
installed = passt-2026_05_26.038c51e-1-x86_64
installed = patch-2.8-1-x86_64
installed = pavucontrol-1:6.2-1-x86_64
installed = pciutils-3.15.0-1-x86_64
installed = pcre-8.45-4-x86_64
installed = pcre2-10.47-1-x86_64
installed = pcsclite-2.5.1-1-x86_64
installed = perl-5.42.2-1-x86_64
installed = perl-error-0.17030-3-any
installed = perl-mailtools-2.22-3-any
installed = perl-timedate-2.35-1-any
installed = pinentry-1.3.2-2-x86_64
installed = pipewire-1:1.6.6-1-x86_64
installed = pipewire-audio-1:1.6.6-1-x86_64
installed = pipewire-pulse-1:1.6.6-1-x86_64
installed = pixman-0.46.4-1-x86_64
installed = pkgconf-2.5.1-1-x86_64
installed = playerctl-2.4.1-5-x86_64
installed = podman-5.8.3-1-x86_64
installed = podman-compose-1.6.0-1-any
installed = podman-desktop-1.27.2-2-x86_64
installed = polkit-127-3-x86_64
installed = polkit-qt6-0.201.1-1-x86_64
installed = poppler-26.05.0-1-x86_64
installed = popt-1.19-2-x86_64
installed = portaudio-1:19.7.0-4-x86_64
installed = portmidi-1:2.0.8-1-x86_64
installed = pps-tools-1.0.3-2-x86_64
installed = procps-ng-4.0.6-1-x86_64
installed = protobuf-35.0-1-x86_64
installed = protobuf-c-1.5.2-10-x86_64
installed = psmisc-23.7-2-x86_64
installed = pugixml-1.15-3-x86_64
installed = python-3.14.5-1-x86_64
installed = python-attrs-26.1.0-1-any
installed = python-autocommand-2.2.2-9-any
installed = python-cffi-2.0.0-2-x86_64
installed = python-click-8.3.3-1-any
installed = python-cryptography-48.0.1-1-x86_64
installed = python-daemonize-2.5.0-10-any
installed = python-dbus-1.4.0-2-x86_64
installed = python-dotenv-1.2.2-1-any
installed = python-filelock-3.29.0-1-any
installed = python-gobject-3.56.3-1-x86_64
installed = python-jaraco.collections-5.1.0-3-any
installed = python-jaraco.context-6.1.2-1-any
installed = python-jaraco.functools-4.1.0-3-any
installed = python-jaraco.text-4.0.0-4-any
installed = python-mako-1.3.11-1-any
installed = python-markdown-3.10.2-1-any
installed = python-markupsafe-3.0.3-1-x86_64
installed = python-more-itertools-11.1.0-1-any
installed = python-numpy-2.4.6-1-x86_64
installed = python-openrazer-3.12.2-2-any
installed = python-packaging-26.2-1-any
installed = python-pip-26.1.2-1-any
installed = python-pkg_resources-81.0.0-1-any
installed = python-platformdirs-4.10.0-1-any
installed = python-protobuf-35.0-1-x86_64
installed = python-pycparser-3.00-1-any
installed = python-pygame-2.6.1-5-x86_64
installed = python-pyudev-0.24.4-1-any
installed = python-setproctitle-1.3.7-2-x86_64
installed = python-setuptools-1:82.0.1-1-any
installed = python-tqdm-4.68.1-1-any
installed = python-typing_extensions-4.15.0-3-any
installed = python-wheel-0.47.0-1-any
installed = python-yaml-6.0.3-2-x86_64
installed = qt5-base-5.15.19+kde+r96-1-x86_64
installed = qt5-declarative-5.15.19+kde+r23-1-x86_64
installed = qt5-graphicaleffects-5.15.19-1-x86_64
installed = qt5-quickcontrols2-5.15.19+kde+r5-1-x86_64
installed = qt5-svg-5.15.19+kde+r5-1-x86_64
installed = qt5-translations-5.15.19-1-any
installed = qt5-wayland-5.15.19+kde+r55-1-x86_64
installed = qt6-5compat-6.11.1-1-x86_64
installed = qt6-base-6.11.1-1-x86_64
installed = qt6-declarative-6.11.1-3-x86_64
installed = qt6-shadertools-6.11.1-1-x86_64
installed = qt6-svg-6.11.1-1-x86_64
installed = qt6-tools-6.11.1-1-x86_64
installed = qt6-translations-6.11.1-1-any
installed = qt6-wayland-6.11.1-1-x86_64
installed = raptor-2.0.16-9-x86_64
installed = rasqal-1:0.9.33-9-x86_64
installed = rav1e-0.8.1-2-x86_64
installed = razergenie-1.3.0-1-x86_64
installed = re2-2:2025.11.05-4-x86_64
installed = readline-8.3.003-1-x86_64
installed = redland-1:1.0.17-10-x86_64
installed = reflector-2023-5-any
installed = resvg-0.47.0-2-x86_64
installed = rhash-1.4.6-1-x86_64
installed = rsync-3.4.4-1-x86_64
installed = rtkit-0.14-1-x86_64
installed = rubberband-4.0.0-2-x86_64
installed = rustup-1.29.0-2-x86_64
installed = sbc-2.2-1-x86_64
installed = scdoc-1.11.4-1-x86_64
installed = sdbus-cpp-2.3.1-1-x86_64
installed = sddm-0.21.0-7-x86_64
installed = sddm-catppuccin-git-r42.10831de-1-any
installed = sdl2-compat-2.32.70-1-x86_64
installed = sdl2_image-2.8.12-1-x86_64
installed = sdl2_mixer-2.8.2-1-x86_64
installed = sdl2_ttf-2.24.0-2-x86_64
installed = sdl3-3.4.10-1-x86_64
installed = sdl3_ttf-3.2.2-3-x86_64
installed = seatd-0.9.3-1-x86_64
installed = sed-4.10-1-x86_64
installed = serd-0.32.10-1-x86_64
installed = shaderc-2026.2-1-x86_64
installed = shadow-4.18.0-1-x86_64
installed = shared-mime-info-2.4-3-x86_64
installed = slang-2.3.3-4-x86_64
installed = slurp-1.5.0-2-x86_64
installed = smartmontools-7.5-1-x86_64
installed = snappy-1.2.2-3-x86_64
installed = sndio-1.10.0-1-x86_64
installed = sord-0.16.22-1-x86_64
installed = sound-theme-freedesktop-0.8-6-any
installed = source-highlight-3.1.9-18-x86_64
installed = spandsp-0.0.6-7-x86_64
installed = spdlog-1.17.0-2-x86_64
installed = speex-1.2.1-2-x86_64
installed = speexdsp-1.2.1-2-x86_64
installed = spirv-tools-1:1.4.350.0-1-x86_64
installed = sqlite-3.53.2-1-x86_64
installed = squashfs-tools-4.7.5-1-x86_64
installed = sratom-0.6.22-1-x86_64
installed = srt-1.5.5-1-x86_64
installed = startup-notification-0.12-9-x86_64
installed = steam-1.0.0.85-7-x86_64
installed = steam-devices-1.0.0.85-7-x86_64
installed = sudo-1.9.17.p2-2-x86_64
installed = svt-av1-4.1.0-1-x86_64
installed = systemd-260.2-2-x86_64
installed = systemd-libs-260.2-2-x86_64
installed = systemd-sysvcompat-260.2-2-x86_64
installed = tar-1.35-2-x86_64
installed = tcl-8.6.16-1-x86_64
installed = tdb-1.4.15-1-x86_64
installed = texinfo-7.3-1-x86_64
installed = thunar-4.20.8-3-x86_64
installed = tinysparql-3.11.1-1-x86_64
installed = tk-8.6.16-1-x86_64
installed = tomlplusplus-3.4.0-2-x86_64
installed = tpm2-tss-4.1.3-1-x86_64
installed = tslib-1.24-1-x86_64
installed = ttf-jetbrains-mono-nerd-3.4.0-2-any
installed = tzdata-2026b-1-x86_64
installed = uchardet-0.0.8-4-x86_64
installed = uefitool-debug-1:0.28.0-3-x86_64
installed = unzip-6.0-23-x86_64
installed = upower-1.91.2-1-x86_64
installed = usbutils-019-1-x86_64
installed = util-linux-2.42.1-1-x86_64
installed = util-linux-libs-2.42.1-1-x86_64
installed = v4l-utils-1.32.0-2-x86_64
installed = vapoursynth-76-1-x86_64
installed = vid.stab-1.1.1-2-x86_64
installed = vim-9.2.0623-1-x86_64
installed = vim-runtime-9.2.0623-1-x86_64
installed = vmaf-3.1.0-1-x86_64
installed = vmware-keymaps-1.0-3-any
installed = vmware-workstation-26H1-3-x86_64
installed = vscodium-bin-1.121.03429-1-x86_64
installed = vulkan-icd-loader-1.4.350.0-1-x86_64
installed = vulkan-tools-1.4.350.0-1-x86_64
installed = waybar-0.15.0-2-x86_64
installed = wayland-1.25.0-1-x86_64
installed = wayland-protocols-1.49-1-any
installed = webrtc-audio-processing-1-1.3-5-x86_64
installed = which-2.25-1-x86_64
installed = winboat-bin-0.9.0-1-x86_64
installed = wine-staging-11.10-1-x86_64
installed = wireplumber-0.5.14-1-x86_64
installed = wl-clipboard-1:2.3.0-1-x86_64
installed = wlogout-1.2.2-0-x86_64
installed = woff2-font-awesome-7.2.0-1-any
installed = wpa_supplicant-2:2.11-5-x86_64
installed = x264-3:0.165.r3222.b35605a-2-x86_64
installed = x265-4.1-1-x86_64
installed = xautomation-1.09-6-x86_64
installed = xcb-proto-1.17.0-4-any
installed = xcb-util-0.4.1-2-x86_64
installed = xcb-util-cursor-0.1.6-1-x86_64
installed = xcb-util-errors-1.0.1-2-x86_64
installed = xcb-util-image-0.4.1-3-x86_64
installed = xcb-util-keysyms-0.4.1-5-x86_64
installed = xcb-util-renderutil-0.3.10-2-x86_64
installed = xcb-util-wm-0.4.2-2-x86_64
installed = xdg-dbus-proxy-0.1.7-1-x86_64
installed = xdg-desktop-portal-1.22.0-1-x86_64
installed = xdg-desktop-portal-gtk-1.15.3-1-x86_64
installed = xdg-desktop-portal-hyprland-1.3.12-2-x86_64
installed = xdg-user-dirs-0.20-1-x86_64
installed = xdg-utils-1.2.1-2-any
installed = xf86-input-libinput-1.5.0-1-x86_64
installed = xfconf-4.20.0-2-x86_64
installed = xkeyboard-config-2.47-1-any
installed = xmlsec-1.3.11-1-x86_64
installed = xorg-fonts-encodings-1.1.0-2-any
installed = xorg-server-21.1.23-1-x86_64
installed = xorg-server-common-21.1.23-1-x86_64
installed = xorg-setxkbmap-1.3.5-1-x86_64
installed = xorg-xauth-1.1.5-1-x86_64
installed = xorg-xkbcomp-1.5.0-1-x86_64
installed = xorg-xprop-1.2.8-1-x86_64
installed = xorg-xrandr-1.5.4-1-x86_64
installed = xorg-xwayland-24.1.12-1-x86_64
installed = xorgproto-2025.1-1-any
installed = xvidcore-1.3.7-3-x86_64
installed = xxhash-0.8.3-1-x86_64
installed = xz-5.8.3-1-x86_64
installed = yaml-cpp-0.9.0-1-x86_64
installed = yay-12.6.0-1-x86_64
installed = yay-debug-12.6.0-1-x86_64
installed = zenity-4.2.2-1-x86_64
installed = zeromq-4.3.5-3-x86_64
installed = zimg-3.0.6-1-x86_64
installed = zint-2.16.0-2-x86_64
installed = zix-0.8.0-1-x86_64
installed = zlib-1:1.3.2-3-x86_64
installed = zlib-ng-2.3.3-1-x86_64
installed = zstd-1.5.7-3-x86_64
installed = zxing-cpp-3.0.2-1-x86_64
@@ -0,0 +1,15 @@
# Generated by makepkg 7.1.0
# using fakeroot version 1.37.2
pkgname = python-simpleline
pkgbase = python-simpleline
xdata = pkgtype=pkg
pkgver = 1.8-1
pkgdesc = Python3 text UI framework (dépendance Anaconda)
url = https://github.com/rhinstaller/python-simpleline
builddate = 1781307804
packager = Unknown Packager
size = 346552
arch = any
license = LGPL-3.0-or-later
depend = python
depend = python-gobject
@@ -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."""
@@ -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
@@ -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
@@ -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()
@@ -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)
@@ -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."""
@@ -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
@@ -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
@@ -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"]
@@ -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)
@@ -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 ""
@@ -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)
@@ -0,0 +1,31 @@
# Module with classes to handle rendering and input.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline.errors import SimplelineError
class RenderError(SimplelineError):
"""Exception raised when error in rendering happens."""
class RenderUnexpectedError(RenderError):
"""Exception raised when something goes really wrong."""
@@ -0,0 +1,253 @@
# Advanced widgets
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
import sys
from simpleline.render import widgets
from simpleline.render.containers import WindowContainer
from simpleline.render.prompt import Prompt
from simpleline.render.screen import UIScreen, InputState
from simpleline.input.input_handler import PasswordInputHandler
from simpleline.utils.i18n import _, N_, C_
__all__ = ["ErrorDialog", "GetInputScreen", "GetPasswordInputScreen", "HelpScreen",
"PasswordDialog", "YesNoDialog"]
class ErrorDialog(UIScreen):
"""Dialog screen for reporting errors to user."""
def __init__(self, message):
"""
:param message: the message to show to the user
:type message: str
"""
super().__init__()
self.title = N_("Error")
self._message = message
def refresh(self, args=None):
super().refresh(args)
text = widgets.TextWidget(self._message)
self.window.add_with_separator(widgets.CenterWidget(text))
def prompt(self, args=None):
return Prompt(_("Press %s to exit") % Prompt.ENTER)
def input(self, args, key):
"""This dialog is closed by any input.
And causes the program to quit.
"""
sys.exit(1)
class PasswordDialog(UIScreen):
"""Dialog screen for password input."""
def __init__(self, message=None):
"""
:param message: password prompt question
:type message: string
"""
super().__init__()
self.title = N_("Password")
self._message = message or _("Enter your passphrase")
self._password = None
def refresh(self, args=None):
super().refresh(args)
text = widgets.TextWidget(self._message)
self.window.add_with_separator(widgets.CenterWidget(text))
def prompt(self, args=None):
handler = PasswordInputHandler(source=self)
if self.password_func:
handler.set_pass_func(self.password_func)
handler.get_input(_("Passphrase: "))
handler.wait_on_input()
if not handler.input_successful():
return None
self._password = handler.value
# this may seem innocuous, but it's really a giant hack; we should
# not be calling close() from prompt(), but the input handling code
# in the TUI is such that without this very simple workaround, we
# would be forever pelting users with a prompt to enter their pw
self.close()
return None
@property
def answer(self):
"""The response can be None (no response) or the password entered."""
return self._password
def input(self, args, key):
if key:
self._password = key
return InputState.PROCESSED_AND_CLOSE
return InputState.DISCARDED
class YesNoDialog(UIScreen):
"""Dialog screen for Yes - No questions."""
def __init__(self, message):
"""
:param message: the message to show to the user
:type message: unicode
"""
super().__init__()
self.title = N_("Question")
self._message = message
self._response = None
def refresh(self, args=None):
super().refresh(args)
text = widgets.TextWidget(self._message)
self.window.add_with_separator(widgets.CenterWidget(text))
def prompt(self, args=None):
return Prompt(_("Please respond '%(yes)s' or '%(no)s'") % {
# TRANSLATORS: 'yes' as positive reply
"yes": C_('TUI|Spoke Navigation', 'yes'),
# TRANSLATORS: 'no' as negative reply
"no": C_('TUI|Spoke Navigation', 'no')
})
def input(self, args, key):
# TRANSLATORS: 'yes' as positive reply
if key == C_('TUI|Spoke Navigation', 'yes'):
self._response = True
return InputState.PROCESSED_AND_CLOSE
# TRANSLATORS: 'no' as negative reply
if key == C_('TUI|Spoke Navigation', 'no'):
self._response = False
return InputState.PROCESSED_AND_CLOSE
return InputState.DISCARDED
@property
def answer(self):
"""The response can be True (yes), False (no) or None (no response)."""
return self._response
class HelpScreen(UIScreen):
"""Screen to display a help message."""
def __init__(self, help_path):
"""
:param help_path: help file name
:type help_path: str
"""
super().__init__()
self.title = N_("Help")
self.help_path = help_path
def refresh(self, args=None):
""" Show the help. """
super().refresh(args)
help_message = _("The help is not available.")
if self.help_path:
with open(self.help_path, 'r') as f:
help_message = f.read()
self.window.add_with_separator(widgets.TextWidget(help_message))
def input(self, args, key):
""" Handle user input. """
return InputState.PROCESSED_AND_CLOSE
def prompt(self, args=None):
return Prompt(_("Press %s to return") % Prompt.ENTER)
class GetInputScreen(UIScreen):
"""Screen for getting user input."""
def __init__(self, message):
"""
:param message: Prompt printed before user input.
:type message: str
"""
super().__init__()
self._message = message
self._value = None
self._conditions = []
@property
def value(self):
"""User input."""
return self._value
def add_acceptance_condition(self, acceptance_function, args=None):
"""Add acceptance condition to the conditions list.
:param acceptance_function: Functions that accepts or rejects a user input.
:type acceptance_function: `function(input, args) -> bool` - function which takes
user input (string) and arguments (`args`) and return True when
input is accepted or False if rejected so we will ask for
a new input.
:param args: Second argument for `acceptance_function` the first one will be user input.
:type args: Anything.
"""
self._conditions.append((acceptance_function, args))
def clear_acceptance_conditions(self):
"""Clear list of the acceptance conditions."""
self._conditions.clear()
def refresh(self, args=None):
super().refresh(args)
self._window = WindowContainer()
def prompt(self, args=None):
return Prompt(message=self._message)
def input(self, args, key):
if not self._test_input(key):
return InputState.DISCARDED
self._value = key
return InputState.PROCESSED_AND_CLOSE
def _test_input(self, key):
for f, args in self._conditions:
if not f(key, args):
return False
return True
class GetPasswordInputScreen(GetInputScreen):
"""Screen for getting user password input."""
def __init__(self, message):
super().__init__(message)
self.hide_user_input = True
@@ -0,0 +1,475 @@
# Widgets for holding other widgets.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
from math import ceil
from simpleline.render.widgets import Widget, TextWidget, SeparatorWidget
from simpleline.logging import get_simpleline_logger
__all__ = ["ListRowContainer", "ListColumnContainer", "WindowContainer"]
log = get_simpleline_logger()
class Container(Widget):
"""Base class for containers which will do positioning of the widgets."""
def __init__(self, items=None, numbering=True):
"""Construct Container.
:param items: List of items for positioning in this Container. Callback
can't be specified this way.
:type items: List of items for rendering.
:param numbering: Enable/disable automatic numbering (labels) for items.
Enabled by default (True).
:type numbering: bool
"""
super().__init__()
self._key_pattern = None
self._items = []
if items:
for i in items:
self._items.append(ContainerItem(i))
if numbering:
self._key_pattern = KeyPattern()
else:
self._key_pattern = None
@property
def size(self):
"""Return items count."""
return len(self._items)
@property
def key_pattern(self):
"""Return key pattern which will be used for items numbering.
Will return `None` if not set.
"""
return self._key_pattern
@key_pattern.setter
def key_pattern(self, key_pattern):
"""Set the key pattern object which will be used for items numbering.
Setting `None` will stop doing numbering.
"""
self._key_pattern = key_pattern
def add(self, item, callback=None, data=None):
"""Add item to the Container.
:param item: Add item to this container.
:type item: Could be item (based on `simpleline.render.widgets.Widget`)
or other container (based on `simpleline.render.containers.Container`).
:param callback: Add callback for this item. This callback will be called when user
activate this `item`.
:type callback: function ``func(data)``.
:param data: Data which will be passed to the callback.
:param data: Anything.
:returns: ID of the item in this Container.
:rtype: int
"""
self._items.append(ContainerItem(item, callback, data))
return len(self._items) - 1
def process_user_input(self, key):
"""Process input from the user if any of the items in the list was called.
This method must be called in `UIScreen.input()` method if list widget should call
the callbacks.
:param key: Key pressed from user.
:type key: str
:returns: True if key was processed. False otherwise.
"""
if not self._key_pattern:
return False
if not isinstance(key, str):
return False
res = self._key_pattern.translate_input_to_widget_id(key)
if res is not None and res >= 0:
try:
item = self._items[res]
if item.callback is not None:
item.callback(item.data)
return True
except IndexError: # container widget with this id doesn't exists
return False
return False
def create_number_label(self, item_id):
"""Create TextWidget from KeyPattern.
:param item_id: Create label for item with this id.
:type item_id: int
:returns: Widget with label for the item with item_id.
:rtype: `simpleline.render.widgets.TextWidget` instance.
"""
number_widget = TextWidget(self._key_pattern.get_widget_label(item_id))
return number_widget
class WindowContainer(Container):
"""Base container for screens.
This can hold other containers or Widgets for rendering.
"""
def __init__(self, title=None):
"""Construct base container for screens.
This container doesn't have numbering support. Input other containers in it to
allow numbering and input processing.
:param title: Title line with separator after this title.
:type title: str
"""
super().__init__(numbering=False)
self._title = title
def add_with_separator(self, item, callback=None, data=None, blank_lines=1):
"""Add widget and after widget add blank line.
This method will call
`self.add(item, callback, data)`
`self.add_separator(lines)`
:param item: Add item to this container.
:type item: Could be item (based on `simpleline.render.widgets.Widget`)
or other container (based on `simpleline.render.containers.Container`).
:param callback: Add callback for this item. This callback will be called when user
activate this `item`.
:type callback: function ``func(data)``.
:param data: Data which will be passed to the callback.
:param data: Anything.
:param blank_lines: How many blank lines should be printed.
:type blank_lines: int greater than 0.
:returns: ID of the item in this Container.
:rtype: int
"""
item_id = self.add(item, callback, data)
self.add_separator(blank_lines)
return item_id
def add_separator(self, lines=1):
"""Add blank lines between widgets.
:param lines: How many blank lines should be printed.
:type lines: int greater than 0.
"""
self.add(SeparatorWidget(lines))
@property
def title(self):
"""Title of WindowContainer."""
return self._title
def render(self, width):
"""Render widgets to it's internal buffer.
:param width: the maximum width the item can use
:type width: int
:return: nothing
"""
super().render(width)
# set cursor position to top-left corner
self.set_cursor_position(0, 0)
if self._title:
self._draw_title_and_separator(width)
for item in self._items:
widget = item.widget
widget.render(width)
self.draw(widget)
def _draw_title_and_separator(self, width):
title_widget = TextWidget(self._title)
sep = SeparatorWidget()
title_widget.render(width)
sep.render(width)
self.draw(title_widget)
self.draw(sep)
class ListRowContainer(Container):
"""Place widgets in rows automatically.
Compared to the ColumnWidget this is able to handle word wrapping correctly.
There is numbering N) automatically for all items. To disable this feature call
`self.key_pattern = None`. If you want other numbering then look on `KeyPattern` class.
Widgets will be placed based on the number of columns in the following way:
1) w1 2) w2 3) w3
4) w4 5) w5 6) w6
....
"""
def __init__(self, columns, items=None, columns_width=None, spacing=3, numbering=True):
"""Create ListWidget with specific number of columns.
:param columns: How many columns we want.
:type columns: int, bigger than 0
:param items: List of items for positioning in this Container. Callback can't be
specified this way.
:type items: List of items for rendering.
:param columns_width: Width of every column. If nothing specified the maximum width
will be distributed to columns.
:type columns_width: int or None
:param spacing: Set the spacing between columns.
:type spacing: int
:param numbering: Enable/disable automatic numbering (labels) for items.
Enabled by default (True).
:type numbering: bool
"""
super().__init__(items, numbering)
self._columns = columns
self._columns_width = columns_width
self._spacing = spacing
self._numbering_widgets = []
def render(self, width):
"""Render widgets to it's internal buffer.
:param width: the maximum width the item can use
:type width: int
:return: nothing
"""
super().render(width)
if self._columns_width is None:
spaces_between_columns = self._columns - 1
sum_spacing = spaces_between_columns * self._spacing
self._columns_width = int((width - sum_spacing) / self._columns)
ordered_map = self._get_ordered_map()
lines_per_rows = self._lines_per_every_row(ordered_map)
# the leftmost empty column
col_pos = 0
for col in ordered_map:
row_pos = 0
# render and draw contents of column
for row_id, item_id in enumerate(col):
container = self._items[item_id]
widget = container.widget
# set cursor to first line and leftmost empty column
self.set_cursor_position(row_pos, col_pos)
if self._key_pattern is not None:
number_widget = self._numbering_widgets[item_id]
widget_width = len(number_widget.text)
self.draw(number_widget)
self.set_cursor_position(row_pos, col_pos + widget_width)
self.draw(widget, block=True)
row_pos = row_pos + lines_per_rows[row_id]
# recompute the leftmost empty column
col_pos = max((col_pos + self._columns_width), self.width) + self._spacing
def _lines_per_every_row(self, items):
self._render_all_items()
# call `self._render_and_calculate_lines_per_rows()` method instead
lines_per_row = []
# go through all items and find how many lines we need for each row
# printed (because of wrapping)
for column_items in items:
for row_id, item_id in enumerate(column_items):
item = self._items[item_id]
if len(lines_per_row) <= row_id:
lines_per_row.append(0)
lines_per_row[row_id] = max(lines_per_row[row_id], len(item.widget.get_lines()))
return lines_per_row
def _render_all_items(self):
for item_id, item in enumerate(self._items):
item_width = self._columns_width
if item_width <= 0:
raise ValueError("Widget can't be rendered! Columns width is too small.")
if self._key_pattern:
number_widget = self.create_number_label(item_id)
# render numbers before widgets
number_width = len(number_widget.text)
number_widget.render(number_width)
self._numbering_widgets.append(number_widget)
# reduce the size of widget because of the number
item_width -= number_width
if item_width <= 0:
raise ValueError("Widget can't be rendered with numbering on! "
"Increase column width or disable numbering.")
item.widget.render(item_width)
def _get_ordered_map(self):
"""Return list of identifiers (index) to the original item list.
.. NOTE: Use of ``self._prepare_list()` is encouraged to create output list and
just fill up this list.
"""
# create list of columns (lists)
ordering_map = self._prepare_list()
for item_id in range(self.size):
ordering_map[item_id % self._columns].append(item_id)
return ordering_map
def _prepare_list(self):
"""Prepare list for items ordering to rows and columns.
List will be prepared as ([column 1], [column 2], ...)
"""
return list(map(lambda x: [], range(0, self._columns)))
class ListColumnContainer(ListRowContainer):
"""Place widgets in columns automatically.
Compared to the ColumnWidget this is able to handle word wrapping correctly.
There is numbering N) automatically for all items. To disable this feature call
`self.key_pattern = None`. If you want other numbering then look on `KeyPattern` class.
Widgets will be placed based on the number of columns in the following way:
1) w1 4) w4 7) w7
2) w2 5) w5 8) w8
3) w3 6) w6 9) w9
"""
def _get_ordered_map(self):
ordering_map = self._prepare_list()
items_in_column = ceil(len(self._items) / self._columns)
for item_id in range(self.size):
col_position = int(item_id // items_in_column)
ordering_map[col_position].append(item_id)
return ordering_map
class KeyPattern():
"""Pattern for automatic key printing before items."""
def __init__(self, pattern="{:d}) ", offset=1):
"""Create the pattern class.
For enabling greater functionality than python 3 format is able to do, feel free to
override this class and use your subclass instead.
:param pattern: Set pattern which will be called for every item.
:type pattern: Strings format method.
See https://docs.python.org/3.3/library/string.html#format-string-syntax.
:param offset: Set the offset for numbering items. Default is 1 to start indexing
naturally for user.
:type offset: int
"""
self._pattern = pattern
self._offset = offset
def get_widget_label(self, item_id):
"""Get widget identifier for user input description.
It should be something similar to the pattern.
:param item_id: Position of the widget in the list.
:type item_id: int starts from 0.
"""
return self._pattern.format(item_id + self._offset)
def translate_input_to_widget_id(self, user_input):
"""Get id of the widget from the user input.
This is reverse translation to `self.get_widget_identifier()`.
:param user_input: Input from user:
:type user_input: str
:return: ID of the widget in the list or None if the input can't be translated.
:rtype: int or None
"""
try:
return int(user_input) - 1
except ValueError:
log.debug("No callback registered for user input %s", user_input)
return None
class ContainerItem():
"""Item used inside of containers to store widgets callbacks and data.
Internal representation for Containers. Do not use this class directly.
"""
def __init__(self, widget, callback=None, data=None):
"""Construct WidgetContainer.
:param widget: Any item from `simpleline.render.widgets` or `Container`.
:type widget: Class subclassing the `simpleline.render.widgets.Widget` class
or `simpleline.render.containers.Container`.
:param callback: This callback will be called as reaction on user input.
:type callback: Function with one data parameter: `def func(data):`.
:param data: Params which will be passed to callback.
:type data: Anything.
"""
self.widget = widget
self.callback = callback
self.data = data
@@ -0,0 +1,154 @@
# Class for the Anaconda TUI prompt.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Vendula Poncova <vponcova@redhat.com>
#
import logging
from simpleline.utils.i18n import N_, _
log = logging.getLogger("simpleline")
class Prompt():
"""Class to create a prompt message with options."""
# Default message of the prompt
DEFAULT_MESSAGE = N_("Please make a selection from the above")
# String to use in a prompt when we want users to press the key ENTER.
ENTER = N_("ENTER")
# TRANSLATORS: 'q' to quit
QUIT_DESCRIPTION = N_("to quit")
QUIT = 'q'
# TRANSLATORS:'c' to continue
CONTINUE_DESCRIPTION = N_("to continue")
CONTINUE = 'c'
# TRANSLATORS:'r' to refresh
REFRESH_DESCRIPTION = N_("to refresh")
REFRESH = 'r'
# TRANSLATORS:'h' to help
HELP_DESCRIPTION = N_("to help")
HELP = 'h'
def __init__(self, message=DEFAULT_MESSAGE):
"""
:param message: the message of the prompt
:type message: str|None
"""
self.message = message
self.options = dict()
def set_message(self, message):
"""Set the prompt message.
:param message: the message of the prompt
:type message: str|None
"""
self.message = message
def add_option(self, key, description):
"""Add an option to the prompt.
Causes a warning if the option already exists.
:param key: the key for choosing the option
:type key: str
:param description: the description of the option
:type description: str
"""
if key in self.options:
log.warning("The option '%s' does already exist in '%s'.", key, self)
self.options[key] = description
def update_option(self, key, description):
"""Update an option in the prompt.
Causes a warning if the option does not exist.
:param key: the key for choosing the option
:type key: str
:param description: the description of the option
:type description: str
"""
if key not in self.options:
log.warning("The option '%s' does not exist in '%s'.", key, self)
self.options[key] = description
def add_refresh_option(self, description=REFRESH_DESCRIPTION):
"""Add the option to refresh."""
if Prompt.REFRESH in self.options:
self.update_option(Prompt.REFRESH, description)
else:
self.add_option(Prompt.REFRESH, description)
def add_continue_option(self, description=CONTINUE_DESCRIPTION):
"""Add the option to continue."""
if Prompt.CONTINUE in self.options:
self.update_option(Prompt.CONTINUE, description)
else:
self.add_option(Prompt.CONTINUE, description)
def add_quit_option(self, description=QUIT_DESCRIPTION):
"""Add the option to quit."""
if Prompt.QUIT in self.options:
self.update_option(Prompt.QUIT, description)
else:
self.add_option(Prompt.QUIT, description)
def add_help_option(self, description=HELP_DESCRIPTION):
"""Add the option to help."""
if Prompt.HELP in self.options:
self.update_option(Prompt.HELP, description)
else:
self.add_option(Prompt.HELP, description)
def remove_option(self, key):
"""Remove an option with the given key.
:param key: the key of the option
:type key: str
:return: the removed option
:rtype: str|None
"""
return self.options.pop(key, None)
def __str__(self):
"""Return the string representation of the prompt."""
if not self.message and not self.options:
return ""
parts = []
if self.message:
parts.append(_(self.message))
if self.options:
opt_list = ["'%s' %s" % (key, _(self.options[key]))
for key in sorted(self.options.keys())]
opt_str = "[%s]" % ", ".join(opt_list)
parts.append(opt_str)
return " ".join(parts) + ": "
@@ -0,0 +1,313 @@
# Base class for text window screens.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from enum import Enum
from simpleline import App
from simpleline.render.containers import WindowContainer
from simpleline.render.prompt import Prompt
from simpleline.render.screen.signal_handler import SignalHandler
from simpleline.render.screen.input_manager import InputManager
from simpleline.utils.i18n import _
__all__ = ["UIScreen", "InputState"]
class UIScreen(SignalHandler):
"""Base class representing one TUI Screen.
Shares some API with anaconda's GUI to make it easy for devs to create similar UI
with the familiar API.
"""
def __init__(self, title=None, screen_height=30):
""" Constructor of the TUI screen.
:param title: Title line of the screen.
:type title: str
:param screen_height: height of the screen (useful for printing long widgets)
:type screen_height: int (the value must be bigger than 4)
"""
self._title = title
self._screen_height = screen_height
self._screen_ready = False
# ask for password
self._hide_user_input = False
self._password_func = None
# do not print separator for this screen
self._no_separator = False
# list that holds the content to be printed out
self._window = WindowContainer(self.title)
# should the input be required after draw
self._input_required = True
# index of the page (subset of screen) shown during show_all
# indexing starts with 0
self._page = 0
self._input_manager = InputManager(ui_screen=self)
def __str__(self):
"""For easier logging."""
return self.__class__.__name__
@property
def title(self):
"""Screen title."""
return self._title
@title.setter
def title(self, title):
"""Set screen title.
Set `None` to remove title.
"""
self._title = title
@property
def password_func(self):
"""Get password function.
This is function with one argument to get password from command line.
"""
return self._password_func
@password_func.setter
def password_func(self, value):
"""Set password function.
:param value: Function to get password from a command line.
:type value: Function with one argument which is text representation of prompt.
"""
self._password_func = value
@property
def screen_ready(self):
"""This screen is ready for use."""
return self._screen_ready
@screen_ready.setter
def screen_ready(self, screen_ready):
"""Set ready status for this screen."""
self._screen_ready = screen_ready
@property
def input_required(self):
"""Return if the screen requires input."""
return self._input_required
@input_required.setter
def input_required(self, input_required):
"""Set if the screen should require input."""
self._input_required = input_required
@property
def no_separator(self):
"""Should we print separator for this screen?
:returns: True to print separator before this screen (default).
False do not print separator.
"""
return self._no_separator
@no_separator.setter
def no_separator(self, no_separator):
"""Print or do not print separator.
:param no_separator: Specify if the separator should be printed.
:type no_separator: bool (default: False).
"""
self._no_separator = no_separator
@property
def hide_user_input(self):
"""Hide typed user input.
This is main solution how to ask for password.
:returns: True if user input should be hidden.
False otherwise (default).
"""
return self._hide_user_input
@hide_user_input.setter
def hide_user_input(self, hide_input):
"""Should be the user input hidden.
:param hide_input: True if user input should be hidden.
False if not (default).
:type hide_input: bool (default: False).
"""
self._hide_user_input = hide_input
@property
def window(self):
"""Return WindowContainer instance."""
return self._window
@window.setter
def window(self, window):
"""Set base WindowContainer instance.
:param window: Base window container containing other widgets and containers.
:type window: Instance of `simpleline.render.containers.WindowContainer` class.
"""
self._window = window
def get_user_input(self, message, hidden=False):
"""Get immediately input from the user.
Use this with cautious. Never call this in middle of rendering or when other
input is already waiting. It is recommended to use `self.input_required` instead.
:param message: Message prompt for the user.
:type message: str
:param hidden: Do not echo user input (password typing).
:type hidden: bool
"""
return self._input_manager.get_input_blocking(message, hidden)
def setup(self, args):
"""Do additional setup right before this screen is used.
It is mandatory to call this ancestor method in the child class to set ready status.
:param args: arguments for the setup
:type args: array of values
:return: whether this screen should be scheduled or not
:rtype: bool
"""
self._screen_ready = True
App.get_event_loop().register_signal_source(self)
return True
def refresh(self, args=None):
"""Method which prepares the content desired on the screen to `self.window`.
:param args: optional argument passed from switch_screen calls
:type args: anything
"""
self.window = WindowContainer(self._title)
def _print_widget(self, widget):
"""Prints a widget with user interaction (when needed).
Could be longer than the screen height.
:param widget: widget to print
:type widget: Widget instance
"""
# TODO: Work even for lower screen_height than 4
pos = 0
lines = widget.get_lines()
num_lines = len(lines)
if num_lines == 0:
return
prompt_height = 2
real_screen_height = self._screen_height - prompt_height
if num_lines < real_screen_height:
# widget plus prompt are shorter than screen height, just print the widget
print(u"\n".join(lines))
return
# long widget, print it in steps and prompt user to continue
last_line = num_lines - 1
while pos <= last_line:
if pos + real_screen_height > last_line:
# enough space to print the rest of the widget plus regular
# prompt (2 lines)
for line in lines[pos:]:
print(line)
pos += self._screen_height - 1
else:
# print part with a prompt to continue
for line in lines[pos:(pos + real_screen_height)]:
print(line)
custom_prompt = Prompt(_("\nPress %s to continue") % Prompt.ENTER)
self._ask_user_input_blocking(custom_prompt)
pos += real_screen_height
def _ask_user_input_blocking(self, prompt):
return self._input_manager.get_input_blocking(prompt, False)
def show_all(self):
"""Print WindowContainer in `self.window` with all its content."""
self.window.render(App.get_configuration().width)
self._print_widget(self.window)
def input(self, args, key):
"""Method called to process input. If the input is not handled here, return it.
:param key: input string to process
:type key: str
:param args: optional argument passed from switch_screen calls
:type args: anything
:return: return `simpleline.render.InputState.PROCESSED` if key was handled,
`simpleline.render.InputState.DISCARDED` if the screen should not process input
on the scheduler and key if you want it to.
:rtype: `simpleline.render.InputState` enum | str
"""
return key
def get_input_with_error_check(self, args):
"""Get user input and redraw if user add too many invalid inputs.
This method should be used only by ScreenScheduler.
:param args: Arguments passed in when scheduling this screen.
:type args: Anything.
"""
self._input_manager.get_input(args=args)
def prompt(self, args=None):
"""Return the text to be shown as prompt or handle the prompt and return None.
:param args: optional argument passed from switch_screen calls
:type args: anything
:return: returns an instance of Prompt with text to be shown next to the prompt
for input or None to skip further input processing
:rtype: Prompt instance|None
"""
prompt = Prompt()
prompt.add_refresh_option()
prompt.add_continue_option()
prompt.add_quit_option()
return prompt
def closed(self):
"""Callback when this screen is closed."""
class InputState(Enum):
PROCESSED = 1
PROCESSED_AND_REDRAW = 2
PROCESSED_AND_CLOSE = 3
DISCARDED = 0
@@ -0,0 +1,203 @@
# Class for managing input and output for application.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from enum import Enum
from simpleline import App
from simpleline.event_loop import ExitMainLoop
from simpleline.event_loop.signals import ExceptionSignal
from simpleline.render.prompt import Prompt
from simpleline.input import InputHandler, PasswordInputHandler
from simpleline.logging import get_simpleline_logger
log = get_simpleline_logger()
class InputManager():
def __init__(self, ui_screen):
"""Processor for user input.
This class is mainly helper class for ScreenScheduler.
:param ui_screen: Screen associated with this input manager.
:type ui_screen: The `simpleline.render.screen.UIScreen` based instance.
"""
super().__init__()
self._ui_screen = ui_screen
self._input_error_counter = 0
self._input_error_threshold = 5
self._input_args = None
@property
def input_error_counter(self):
"""Return how many times the user provided bad input."""
return self._input_error_counter
@property
def input_error_threshold_exceeded(self):
"""Did the error counter pass the threshold?
The screen should be redraw.
"""
errors = self._input_error_counter % self._input_error_threshold
return errors == 0
def get_input_blocking(self, message, hidden):
"""Get blocking input from the user.
:param message: Message prompt for the user.
:type message: str
:param hidden: Do not echo user input (password typing).
:type hidden: bool
"""
if hidden:
handler = PasswordInputHandler(source=self)
if self._ui_screen.password_func:
handler.set_pass_func(self._ui_screen.password_func)
else:
handler = InputHandler(source=self)
handler.get_input(message)
handler.wait_on_input()
return handler.value
def get_input(self, args=None):
"""Get input from user.
:param args: Arguments passed in when UIScreen was scheduled.
:type args: Anything.
"""
prompt = self._ui_screen.prompt(args)
if not self._is_input_expected(prompt):
return
self._input_args = args
if not self._ui_screen.hide_user_input:
handler = InputHandler(source=self._ui_screen)
else:
handler = PasswordInputHandler(source=self._ui_screen)
if self._ui_screen.password_func:
handler.set_pass_func(self._ui_screen.password_func)
handler.set_callback(self.process_input)
handler.get_input(prompt)
def _is_input_expected(self, prompt):
"""Check if user handled input processing some other way.
Do nothing if user did handled user input.
:returns: True if prompt is set and we can use it to get user input.
False if prompt is not available, which means that user handled input on their
own.
"""
# None means prompt handled the input by itself -> continue
if prompt is None:
self._input_error_counter = 0
return False
return True
def process_input(self, user_input):
"""Process input from the screens.
:param user_input: User input string.
:type user_input: String.
:raises: ExitMainLoop or any other kind of exception from screen processing.
"""
# process the input, if it wasn't processed (valid)
# increment the error counter
try:
result = self._process_input(user_input)
except ExitMainLoop: # pylint: disable=try-except-raise
raise
except Exception: # pylint: disable=broad-except
App.get_event_loop().enqueue_signal(ExceptionSignal(self))
return
if result.was_successful():
self._input_error_counter = 0
else:
self._input_error_counter += 1
App.get_scheduler().process_input_result(result, self.input_error_threshold_exceeded)
def _process_input(self, key):
"""Method called internally to process unhandled input key presses.
:param key: The string entered by user.
:type key: String.
:return: Return state result object.
:rtype: `simpleline.render.in_out_manager.UserInputResult` class.
:raises: Anything the Screen can raise in the input processing.
"""
from simpleline.render.screen import InputState # pylint: disable=import-outside-toplevel
# delegate the handling to active screen first
key = self._ui_screen.input(self._input_args, key)
if key == InputState.PROCESSED:
return UserInputAction.NOOP
if key == InputState.PROCESSED_AND_REDRAW:
return UserInputAction.REDRAW
if key == InputState.PROCESSED_AND_CLOSE:
return UserInputAction.CLOSE
if key == InputState.DISCARDED:
return UserInputAction.INPUT_ERROR
# global refresh command
if key == Prompt.REFRESH:
return UserInputAction.REDRAW
# global close command
if key == Prompt.CONTINUE:
return UserInputAction.CLOSE
# global quit command
if key == Prompt.QUIT:
return UserInputAction.QUIT
if key is None:
log.warning("Returned key from screen is None. "
"This could be missing return in a screen input method?")
return UserInputAction.INPUT_ERROR
class UserInputAction(Enum):
"""Store user input result."""
INPUT_ERROR = -1
NOOP = 0
REDRAW = 5
CLOSE = 6
QUIT = 7
def was_successful(self):
return self != UserInputAction.INPUT_ERROR
@@ -0,0 +1,94 @@
# Signal handler is giving ability connect and emit to all widgets.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline import App
from simpleline.event_loop.signals import RenderScreenSignal, CloseScreenSignal
class SignalHandler():
"""Provides methods for handling signals anc callbacks.
Provides main methods:
`connect()` -- connect this widget on given signal
`create_signal()` -- create signal class which can be used in the emit method
`emit()` -- emit signal given signal
"""
def connect(self, signal, callback, data=None):
"""Connect this class method with given signal.
:param signal: signal class which you want to connect
:type signal: class based on `simpleline.event_loop.AbstractSignal`
:param callback: the callback function
:type callback: func(event_message, data)
:param data: Data you want to pass to the callback
:type data: Anything
"""
App.get_event_loop().register_signal_handler(signal, callback, data)
def create_signal(self, signal_class, priority=0):
"""Create signal instance usable in the emit method.
:param signal_class: signal you want to use
:type signal_class: class based on `simpleline.event_loop.AbstractSignal`
:param priority: priority of the signal; please look on the
`simpleline.event_loop.AbstractSignal.priority` for further info
:type priority: int
"""
return signal_class(self, priority)
def emit(self, signal):
"""Emit the signal.
This will add `signal` to the event loop.
:param signal: signal to emit
:type signal: instance of class based on `simpleline.event_loop.AbstractSignal`
"""
App.get_event_loop().enqueue_signal(signal)
def create_and_emit(self, signal):
"""Create the signal and emit it.
This is basically shortcut for calling `self.create_signal` and `self.emit`.
"""
created_signal = self.create_signal(signal)
self.emit(created_signal)
def redraw(self):
"""Emit signal to initiate draw.
Add RenderScreenSignal to the event loop.
"""
signal = self.create_signal(RenderScreenSignal)
App.get_event_loop().enqueue_signal(signal)
def close(self):
"""Emit signal to close this screen.
Add CloseScreenSignal to the event loop.
"""
signal = self.create_signal(CloseScreenSignal)
App.get_event_loop().enqueue_signal(signal)
@@ -0,0 +1,58 @@
# Serves shortcuts for easy screen scheduling.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline import App
class ScreenHandler():
@classmethod
def schedule_screen(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.schedule_screen()`.
"""
App.get_scheduler().schedule_screen(ui_screen=ui_screen, args=args)
@classmethod
def replace_screen(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.replace_screen()`.
"""
App.get_scheduler().replace_screen(ui_screen=ui_screen, args=args)
@classmethod
def push_screen(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.push_screen()`.
"""
App.get_scheduler().push_screen(ui_screen=ui_screen, args=args)
@classmethod
def push_screen_modal(cls, ui_screen, args=None):
"""Schedule screen to the active scheduler.
See: `simpleline.render.screen_scheduler.push_screen_modal()`.
"""
App.get_scheduler().push_screen_modal(ui_screen=ui_screen, args=args)
@@ -0,0 +1,311 @@
# Class handling rendering of the screens to console.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
import threading
from simpleline import App
from simpleline.event_loop import ExitMainLoop
from simpleline.event_loop.signals import ExceptionSignal, RenderScreenSignal, CloseScreenSignal
from simpleline.render import RenderUnexpectedError
from simpleline.render.screen.input_manager import UserInputAction
from simpleline.render.screen_stack import ScreenStack, ScreenData, ScreenStackEmptyException
from simpleline.logging import get_simpleline_logger
log = get_simpleline_logger()
RAW_INPUT_LOCK = threading.Lock()
__all__ = ["ScreenScheduler"]
class ScreenScheduler():
def __init__(self, event_loop, scheduler_stack=None):
"""Constructor where you can pass your own scheduler stack.
The ScreenStack will be used automatically if scheduler stack will be None.
:param event_loop: Event loop used for the scheduler.
:type event_loop: Class based on `simpleline.event_loop.AbstractEventLoop`.
:param scheduler_stack: Use custom scheduler stack if you need to.
:type scheduler_stack: `simpleline.screen_stack.ScreenStack` based class.
"""
self._quit_screen = None
self._event_loop = event_loop
if scheduler_stack:
self._screen_stack = scheduler_stack
else:
self._screen_stack = ScreenStack()
self._register_handlers()
self._first_screen_scheduled = False
@staticmethod
def _spacer():
return "\n".join(2 * [App.get_configuration().width * "="])
def _register_handlers(self):
self._event_loop.register_signal_handler(RenderScreenSignal, self._process_screen_callback)
self._event_loop.register_signal_handler(CloseScreenSignal, self._close_screen_callback)
@property
def quit_screen(self):
"""Return quit UIScreen."""
return self._quit_screen
@quit_screen.setter
def quit_screen(self, quit_screen):
"""Set the UIScreen based instance which will be showed before the Application will quit.
You can also use `simpleline.render.adv_widgets.YesNoDialog` or `UIScreen` based class
with the `answer` property. Without the `answer` property the application will always
close.
"""
self._quit_screen = quit_screen
@property
def nothing_to_render(self):
"""Is something for rendering in the scheduler stack?
:return: True if the rendering stack is empty
:rtype: bool
"""
return self._screen_stack.empty()
def dump_stack(self):
"""Get string representation of actual screen stack."""
return self._screen_stack.dump_stack()
def schedule_screen(self, ui_screen, args=None):
"""Add screen to the bottom of the stack.
This is mostly useful at the beginning to prepare the first screen hierarchy to display.
:param ui_screen: screen to show
:type ui_screen: UIScreen instance
:param args: optional argument, please see switch_screen for details
:type args: anything
"""
log.debug("Scheduling screen %s", ui_screen)
screen = ScreenData(ui_screen, args)
self._screen_stack.add_first(screen)
self._redraw_on_first_scheduled_screen()
def _redraw_on_first_scheduled_screen(self):
if not self._first_screen_scheduled:
self.redraw()
self._first_screen_scheduled = True
def replace_screen(self, ui_screen, args=None):
"""Schedules a screen to replace the current one.
:param ui_screen: screen to show
:type ui_screen: instance of UIScreen
:param args: optional argument to pass to ui's refresh and setup methods
(can be used to select what item should be displayed or so)
:type args: anything
"""
log.debug("Replacing screen %s", ui_screen)
try:
execute_new_loop = self._screen_stack.pop().execute_new_loop
except ScreenStackEmptyException as e:
raise ScreenStackEmptyException("Switch screen is not possible when there is no "
"screen scheduled!") from e
# we have to keep the old_loop value so we stop
# dialog's mainloop if it ever uses switch_screen
screen = ScreenData(ui_screen, args, execute_new_loop)
self._screen_stack.append(screen)
self.redraw()
def push_screen(self, ui_screen, args=None):
"""Schedules a screen to show, but keeps the current one in stack to
return to, when the new one is closed.
:param ui_screen: screen to show
:type ui_screen: UIScreen instance
:param args: optional argument
:type args: anything
"""
log.debug("Pushing screen %s to stack", ui_screen)
screen = ScreenData(ui_screen, args, False)
self._screen_stack.append(screen)
self.redraw()
def push_screen_modal(self, ui_screen, args=None):
"""Starts a new screen right away, so the caller can collect data back.
When the new screen is closed, the caller is redisplayed.
This method does not return until the new screen is closed.
:param ui_screen: screen to show
:type ui_screen: UIScreen instance
:param args: optional argument, please see switch_screen for details
:type args: anything
"""
log.debug("Pushing modal screen %s to stack", ui_screen)
screen = ScreenData(ui_screen, args, True)
self._screen_stack.append(screen)
# only new events will be processed now
# the old one will wait after this event loop will be closed
self._event_loop.execute_new_loop(RenderScreenSignal(self))
def _close_screen_callback(self, signal, data):
self.close_screen(signal.source)
def close_screen(self, closed_from=None):
"""Close the currently displayed screen and exit it's main loop if necessary.
Next screen from the stack is then displayed.
"""
screen = self._screen_stack.pop()
log.debug("Closing screen %s from %s", screen, closed_from)
# User can react when screen is closing
screen.ui_screen.closed()
if closed_from is not None and closed_from is not screen.ui_screen:
raise RenderUnexpectedError("You are trying to close screen %s from screen %s! "
"This is most probably not intentional." %
(closed_from, screen.ui_screen))
if screen.execute_new_loop:
self._event_loop.close_loop()
# redraw screen if there is what to redraw
# and if it is not modal screen (modal screen parent is blocked)
if not self._screen_stack.empty() and not screen.execute_new_loop:
self.redraw()
# we can't draw anything more. Kill the application.
if self._screen_stack.empty():
raise ExitMainLoop()
def redraw(self):
"""Register rendering to the event loop for processing."""
self._event_loop.enqueue_signal(RenderScreenSignal(self))
def _process_screen_callback(self, signal, data):
self._process_screen()
def _process_screen(self):
"""Process the current screen.
1) It will call setup if the screen is not already set.
2a) If setup was success then draw the screen.
2b) If setup wasn't successful then pop the screen and try to process next in the stack.
Continue by (1).
3)Ask for user input if requested.
"""
top_screen = self._get_last_screen()
log.debug("Processing screen %s", top_screen)
# this screen is used first time (call setup() method)
if not top_screen.ui_screen.screen_ready:
if not top_screen.ui_screen.setup(top_screen.args):
# remove the screen and skip if setup went wrong
self._screen_stack.pop()
self.redraw()
log.warning("Screen %s setup wasn't successful", top_screen)
return
# get the widget tree from the screen and show it in the screen
try:
# refresh screen content
top_screen.ui_screen.refresh(top_screen.args)
# Screen was closed in the refresh method
if top_screen != self._get_last_screen():
return
# draw screen to the console
self._draw_screen(top_screen)
if top_screen.ui_screen.input_required:
log.debug("Input is required by %s screen", top_screen)
top_screen.ui_screen.get_input_with_error_check(top_screen.args)
except ExitMainLoop: # pylint: disable=try-except-raise
raise
except Exception: # pylint: disable=broad-except
self._event_loop.enqueue_signal(ExceptionSignal(self))
return
def _draw_screen(self, active_screen):
"""Draws the current `active_screen`.
:param active_screen: Screen which should be draw to the console.
:type active_screen: Classed based on `simpleline.render.screen.UIScreen`.
"""
# get the widget tree from the screen and show it in the screen
try:
if not active_screen.ui_screen.no_separator:
# separate the content on the screen from the stuff we are about to display now
print(self._spacer())
# print UIScreen content
active_screen.ui_screen.show_all()
except ExitMainLoop: # pylint: disable=try-except-raise
raise
except Exception: # pylint: disable=broad-except
self._event_loop.enqueue_signal(ExceptionSignal(self))
def _get_last_screen(self):
if self._screen_stack.empty():
raise ExitMainLoop()
return self._screen_stack.pop(False)
def process_input_result(self, input_result, should_redraw):
active_screen = self._get_last_screen()
if not input_result.was_successful():
if should_redraw:
self.redraw()
else:
log.debug("Input was not successful, ask for new input.")
active_screen.ui_screen.get_input_with_error_check(active_screen.args)
else:
if input_result == UserInputAction.NOOP:
return
if input_result == UserInputAction.REDRAW:
self.redraw()
elif input_result == UserInputAction.CLOSE:
self.close_screen()
elif input_result == UserInputAction.QUIT:
if self.quit_screen:
self.push_screen_modal(self.quit_screen)
try:
if self.quit_screen.answer is True:
raise ExitMainLoop()
self.redraw()
except AttributeError as e:
raise ExitMainLoop() from e
else:
raise ExitMainLoop()
@@ -0,0 +1,117 @@
# Classes implementation for storing and manipulating Screen stack.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
# Author(s): Jiri Konecny <jkonecny@redhat.com>
#
from simpleline.errors import SimplelineError
class ScreenStackException(SimplelineError):
"""General screen stack exception."""
class ScreenStackEmptyException(ScreenStackException):
"""Screen stack exception when stack is empty."""
class ScreenStack():
"""Managing screen stack used in `ScreenScheduler`."""
def __init__(self):
self._screens = []
def empty(self):
"""Test if screen stack is empty.
:return: True if empty.
:rtype: bool
"""
return not self._screens
def size(self):
"""Get size of the stack.
:return: Size of the stack.
"""
return len(self._screens)
def append(self, screen):
"""Add new screen to the top of the stack.
:param screen: Screen for the future rendering.
:type screen: Class based on `simpleline.render.ui_screen.UIScreen`.
"""
self._screens.append(screen)
def pop(self, remove=True):
"""Return top item from the stack.
:param remove: If True (default) also remove this items from the stack.
:return: The top screen on the stack.
"""
try:
if remove:
return self._screens.pop()
return self._screens[-1]
except IndexError as e:
raise ScreenStackEmptyException(e) from e
def add_first(self, screen):
"""Add `screen` to the bottom of the stack.
:param screen: Add the `screen` to the bottom of the stack.
:type screen: Class based on `simpleline.render.ui_screen.UIScreen`.
"""
self._screens.insert(0, screen)
def dump_stack(self):
"""Dump screen stack structure.
:returns: Screen stack representation.
:rtype: str
"""
msg = '======= Screen stack =======\n'
msg += '----------- TOP ------------\n'
for screen in reversed(self._screens):
msg += str(screen)
msg += "\n"
msg += '============================\n'
return msg
class ScreenData():
"""Inner data class to store screen data."""
def __init__(self, ui_screen, args=None, execute_new_loop=False):
self.ui_screen = ui_screen
self.args = args
self.execute_new_loop = execute_new_loop
def __str__(self):
msg = self.__class__.__name__
msg += "("
msg += ",".join((str(self.ui_screen), str(self.args), str(self.execute_new_loop)))
msg += ")"
return msg
@@ -0,0 +1,495 @@
# Widgets for Text UI framework.
#
# This file is part of Simpleline Text UI library.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Simpleline is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Simpleline is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Simpleline. If not, see <https://www.gnu.org/licenses/>.
#
import functools
from textwrap import wrap
from simpleline.utils.i18n import _
from simpleline.utils import ensure_str
__all__ = ["Widget", "TextWidget", "SeparatorWidget", "EntryWidget", "ColumnWidget",
"CheckboxWidget", "CenterWidget"]
class Widget():
def __init__(self, max_width=None, default=None):
"""Initializes base Widgets buffer.
This class can be subclassed to create customized widgets.
:param max_width: serves as a hint about screen size to write method with default arguments
:type max_width: int
:param default: string containing the default content to fill the buffer with
:type default: string
"""
self._buffer = []
if default:
self._buffer = [[c for c in l] for l in default.split("\n")] # pylint: disable=unnecessary-comprehension
self._max_width = max_width
self._cursor = (0, 0) # row, col
@property
def height(self):
"""The current height of the internal buffer."""
return len(self._buffer)
@property
def width(self):
"""The current width of the internal buffer (id of the first empty column)."""
return functools.reduce(lambda acc, l: max(acc, len(l)), self._buffer, 0)
def clear(self):
"""Clears this widgets buffer and resets cursor."""
self._buffer = list()
self._cursor = (0, 0)
@property
def content(self):
"""Return a list (rows) of lists (columns) with one character elements."""
return self._buffer
def render(self, width):
"""Redraw the widget's self._buffer.
:param width: the width of buffer requested by the caller
:type width: int
Commonly, call render of child widgets and then draw and write
methods to copy their contents to self._buffer.
"""
self.clear()
def get_lines(self):
"""Return lines to write out in order to show this widget.
:return: lines representing this widget
:rtype: list(str)
"""
return [str(u"".join(line)) for line in self._buffer]
def set_cursor_position(self, row, col):
"""Set cursor position.
:param row: row id, starts with 0 at the top of the screen
:type row: int
:param col: column id, starts with 0 on the left side of the screen
:type col: int
"""
self._cursor = (row, col)
@property
def cursor(self):
return self._cursor
def set_end(self):
"""Set the cursor to first column in new line at the end."""
self._cursor = (self.height, 0)
def draw(self, w, row=None, col=None, block=False):
"""Copy w widget's content to this widget's buffer at row, col position.
:param w: widget to take content from
:type w: class Widget
:param row: row number to start at (default is at the cursor position)
:type row: int
:param col: column number to start at (default is at the cursor position)
:type col: int
:param block: when printing newline, start at column col (True) or at column 0 (False)
:type block: boolean
"""
# if the starting row is not present, start at the cursor position
if row is None:
row = self._cursor[0]
# if the starting column is not present, start at the cursor position
if col is None:
col = self._cursor[1]
# fill up rows to accommodate for w.height
if self.height < row + w.height:
for _i in range(row + w.height - self.height):
self._buffer.append(list())
# append columns to accommodate for w.width
for l in range(row, row + w.height):
l_len = len(self._buffer[l])
w_len = len(w.content[l - row])
if l_len < col + w_len:
self._buffer[l] += ((col + w_len - l_len) * list(u" "))
self._buffer[l][col:col + w_len] = w.content[l - row][:]
# move the cursor to new spot
if block:
self._cursor = (row + w.height, col)
else:
self._cursor = (row + w.height, 0)
def write(self, text, row=None, col=None, width=None, block=False, wordwrap=False):
"""Emulate the typing machine writing to this widget's buffer.
:param text: text to type
:type text: str
:param row: row number to start at (default is at the cursor position)
:type row: int
:param col: column number to start at (default is at the cursor position)
:type col: int
:param width: wrap at "col" + "width" column (default is at self._max_width)
:type width: int
:param block: when printing newline, start at column col (True) or at column 0 (False)
:type block: boolean
:param wordwrap: wrap by words
:type wordwrap: boolean
"""
if not text:
return
text = ensure_str(text)
if row is None:
row = self._cursor[0]
if col is None:
col = self._cursor[1]
if width is None and self._max_width:
width = self._max_width - col
x = row
y = col
if wordwrap:
text = self._wrap_words(text, width)
# emulate typing machine
for character in text:
# FIXME: Remove the code duplication below and optimize it
# process newline
if character == "\n":
x += 1
if block:
y = col
else:
y = 0
self._increase_x_buffer_size(x)
continue
self._increase_x_buffer_size(x)
self._increase_y_buffer_size(x, y)
self._save_character_to_buffer(x, y, character)
# shift to the next char
y += 1
if width is not None and y >= col + width:
x += 1
if block:
y = col
else:
y = 0
self._cursor = (x, y)
def _increase_x_buffer_size(self, x):
if x >= len(self._buffer):
for _i in range(x - len(self._buffer) + 1):
self._buffer.append(list())
def _increase_y_buffer_size(self, x, y):
if y >= len(self._buffer[x]):
self._buffer[x] += ((y - len(self._buffer[x]) + 1) * list(u" "))
def _save_character_to_buffer(self, x, y, character):
self._buffer[x][y] = character
@staticmethod
def _wrap_words(text, width):
lines = []
# Wrap each line separately
for line in text.split('\n'):
sublines = []
for subline in wrap(line, width):
sublines.append(subline)
if len(subline) < width:
# line shorter than width will be wrapped by '\n' we add
sublines.append('\n')
# line with length == width will be wrapped by the width based
# wrapping logic
# end of line will be wrapped by '\n' following the line in
# original text
if sublines and sublines[-1] == '\n':
sublines.pop()
lines.append("".join(sublines))
return '\n'.join(lines)
class TextWidget(Widget):
"""Class to handle wrapped text output."""
def __init__(self, text):
"""
:param text: text to format
:type text: str
"""
super().__init__()
self._text = text
@property
def text(self):
"""Contains text of this widget."""
return self._text
def render(self, width):
"""Renders the text widget limited to width number of columns.
Wraps to the next line when the text is longer.
:param width: maximum width allocated to the string
:type width: int
"""
super().render(width)
self.write(self._text, width=width, wordwrap=True)
class EntryWidget(TextWidget):
"""This is the easy way how to generate entry items for containers.
If the numbering in a container is turned on the output looks like:
N) title
value
Without numbering turned on:
title
value
"""
def __init__(self, title, value=None):
""" Create Entry widget instance.
:param title: Title of this entry.
:type title: String.
:param value: Actual value printed in second line below the title.
:type value: String.
"""
text = self._create_text(title=title, value=value)
super().__init__(text)
@staticmethod
def _create_text(title, value):
msg = title
if value:
msg += "\n"
msg += value
return msg
class SeparatorWidget(Widget):
"""Print empty line."""
def __init__(self, lines=1):
"""Construct SeparatorWidget for printing blank lines.
:param lines: How many lines should be blank.
:type lines: int greater than 0.
"""
super().__init__()
self._lines = lines
def render(self, width):
"""Render empty line to the buffer.
:param width: maximum width allocated to the string
:type width: int
"""
super().render(width)
self.write("")
def write(self, text, row=None, col=None, width=None, block=False, wordwrap=False):
"""Optimize write function.
To print just a blank line we don't need too much logic.
"""
for i in range(0, self._lines):
self._buffer.append(list())
self._buffer[i] += u""
self.set_cursor_position(self._lines - 1, 0)
class CenterWidget(Widget):
"""Class to handle horizontal centering of content."""
def __init__(self, w):
"""
:param w: widget to center
:type w: Widget
"""
super().__init__()
self._w = w
def render(self, width):
"""Render the centered widget to internal buffer.
:param width: maximum width the widget should use
:type width: int
"""
super().render(width)
self._w.render(width)
# make sure col is an integer
self.draw(self._w, col=(width - self._w.width) // 2)
class CheckboxWidget(Widget):
"""Widget to show checkbox with (un)checked box, name and description."""
def __init__(self, key="x", title=None, text=None, completed=None):
"""
:param key: tick character to be used inside [ ]
:type key: character
:param title: the title next to the [ ] box
:type title: str
:param text: the description text to be shown on the second row in ()
:type text: str
:param completed: is the checkbox ticked or not?
:type completed: True|False
"""
super().__init__()
self._key = key
self._title = title
self._text = text
self._completed = completed
def render(self, width):
"""Render the widget to internal buffer.
It should be max width characters wide.
"""
super().render(width)
if self.completed:
checkchar = self._key
else:
checkchar = " "
# prepare the checkbox
checkbox = TextWidget("[%s]" % checkchar)
data = []
# append lines
if self.title:
data.append(TextWidget(_(self.title)))
if self.text:
data.append(TextWidget("(%s)" % self.text))
# the checkbox has two columns
# [x] is one and is 3 chars wide
# text is second and can occupy width - 3 - 1 (for space) chars
cols = ColumnWidget([(3, [checkbox]), (width - 4, data)], 1)
cols.render(width)
# transfer the column widget rendered stuff to internal buffer
self.draw(cols)
@property
def title(self):
"""Returns the first line (main title) of the checkbox."""
return self._title
@property
def completed(self):
"""Returns the state of the checkbox, checked is True."""
return self._completed
@property
def text(self):
"""Contains the description text from the second line."""
return self._text
class ColumnWidget(Widget):
def __init__(self, columns, spacing=0):
"""Create text columns
Deprecated. Please do not use this widget, use containers instead.
:param columns: list containing (column width, [list of widgets to put into this column])
:type columns: [(int, [...]), ...]
:param spacing: number of spaces to use between columns
:type spacing: int
"""
super().__init__()
self._spacing = spacing
self._columns = columns
def render(self, width):
"""Render the widget to it's internal buffer
:param width: the maximum width the widget can use
:type width: int
:return: nothing
"""
super().render(width)
# the leftmost empty column
col_pos = 0
# iterate over tuples (column width, column content)
for col_width, col in self._columns:
# set cursor to first line and leftmost empty column
self.set_cursor_position(0, col_pos)
# if requested width is None, limit the maximum to width
# and set minimum to 0
if col_width is None:
col_max_width = width - self.cursor[1]
col_width = 0
else:
col_max_width = col_width
# render and draw contents of column
for item in col:
item.render(col_max_width)
self.draw(item, block=True)
# recompute the leftmost empty column
col_pos = max((col_pos + col_width), self.width) + self._spacing
@@ -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)
@@ -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
@@ -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.