Giter Site home page Giter Site logo

jwlodek / py_cui Goto Github PK

View Code? Open in Web Editor NEW
738.0 14.0 41.0 20.16 MB

A python library for intuitively creating CUI/TUI interfaces with widgets, inspired by gocui.

Home Page: https://jwlodek.github.io/py_cui-docs

License: BSD 3-Clause "New" or "Revised" License

Python 100.00%
cui tui terminal terminal-based python python-library command-line commandline-interface command

py_cui's Introduction

PyCUI


A library for creating CUI/TUI interfaces with pre-built widgets in python.

Easily build text-based user interfaces on both Linux and Windows.

Automated Test+Build Status Download Counter PyPI Version License

py_cui

py_cui is a python library meant to simplify writing console user interfaces in python. It relies on curses for terminal rendering, which is traditionally unix-specific, however, you may use the windows-curses module to run py_cui on windows.

The main advantage py_cui has over typical text-based user interface builders is that it relies on widgets and a grid layout manager like most traditional graphical user interfaces. You may define a grid size, and then drop predefined widgets onto it into specific grid locations. Widgets can also be stretched accross multiple grid rows and columns. If you've ever made a Tkinter GUI, you will feel right at home. In addition, py_cui has support for a rich collection of interactive popups such as menus, forms, file dialogs, and more.

If you would like to contribute, feel free to make an issue or pull request after reading through the CONTRIBUTING.md file.

Installation

You may install py_cui via pip (use pip3 if python 2 and 3 are installed side by side)

pip install py-cui

Make sure to run as root/sudo as required. Note that the library is titled py_cui, but the name of the pypi package is py-cui, though in most cases, installing with

pip install py_cui

should work OK.

If you would like to use py_cui directly from source, clone this repository with:

git clone https://github.com/jwlodek/py_cui

Then build/install with pip:

cd py_cui
pip install .

If you would like to try it out without installing, first make sure that curses is installed (windows-curses if on windows), and then copy one of the example files into the top directory. You may then simply run it with python3.

Examples

Below are some screenshots and gifs of programs written with py_cui. Additional (simpler) examples are available in the examples/ directory of this repository.

py_cui_2048 - A command line version of the classic 2048 game

py_cui_2048 is a demo of using py_cui to build simple CUI games and applications. The entire game was made in under 400 lines of code.

recoverpy - A TUI that allows for recovering lost or deleted files

recoverpy is a utility for recovering files on linux. It will search through every inode to find your requested file block.

pyautogit - A command line interface for managing git repositories

pyautogit is the first larger scale project written in py_cui. Feel free to use it as a guide to programming with py_cui.

Getting started

The easiest way to get set up for developing a full py_cui application is to use the available cookiecutter template. First, install the cookiecutter tool:

pip install cookiecutter

Once it is installed, navigate to the directory in which you would like your projects to live, and run:

cookiecutter https://github.com/jwlodek/py_cui_cookiecutter

This will clone the template, and show prompts for several variables, including project name and description, as well as some developer information. Once it is done, you can enter your project directory, install it with pip, and run it, and you should be able to see a Hello World example:

cd myproject
pip install .
myproject

You are now ready to extend this template for your own application!

Writing a PyCUI

Basic usage of py_cui starts with creating a PyCUI object, and specifiying it's grid size. Keep in mind that grid cell height and width will be measured in terminal characters, not pixels, so there is a lower limit on legal grid size, and heights will be smaller values than widths. Create this object with:

root = py_cui.PyCUI(7, 9)

The above line will create a UI with 7 rows and 9 columns. Then, add widgets with the different add commands:

label = root.add_label('Label Text', 0, 0)
button = root.add_button('Button Text', 1, 2, column_span=2, command=my_function)
...

Finally, start the CUI with

root.start()

py_cui has support for custom key bindings for both the overview mode and focused mode, popup windows and prompts, color rendering rules, and several useful widgets. For more details on writing py_cui based interfaces, be sure to check on the examples and the documentation here.

Using a PyCUI

There are some basic rules that apply to all py_cui based interfaces. There are three key operating modes - overview mode, focus mode, and popup mode.

Overview Mode

Overview mode is the main control view of the interface. In this mode you use the arrow keys to move between widgets, and you may select widgets with the Enter key. By default, you may also press buttons in this mode, unless auto_focus_buttons is set to false when the CUI was created.

Focus Mode

When in focus mode, you enter into a particular widget (For example a text box.). Each widget has some predefined basic controls, such as arrow keys to scroll in a ScrollMenu. You may also add keybindings to functions for each particular widget. These keybindings will only apply to a widget if it is in focus mode. Enter focus mode by navigating to a widget in overview mode and hitting the Enter key. Return to overview mode from focus mode by pressing Escape.

Popup Mode

Popup mode simply displays a popup over the rest of the UI. Each popup type behaves slightly differently. For example, you may exit an info popup mode by pressing Space, Enter, or Escape, while a loading popup will remain onscreen until whatever operation is being run is terminated.

Unit Tests

py_cui unit tests are written for pytest. Make sure pytest is installed, and simply run

pytest

in the root directory to run all unit tests.

Powered by py_cui

Below is a table of python projects developed with the help of py_cui. If you've made something, feel free to make a pull request to add it!

Project Description
pypodcasts A TUI for listening to and managing podcast feeds. (Coming Soon)
pyautogit A command line UI for interfacing with git features for multiple repositories.
recoverpy A CUI for recovering overwritten or deleted data on linux.
py_cui_2048 A CUI version of the classic 2048 game.
unipkg A CUI interface for managing all installed package managers on your system. (In-Progress)
CUIAudioPlayer A TUI for playing back audio files in a variety of formats.
tinypub A console-based ebook reader.
hue-tui A CUI controlling for Philips Hue lights.
mini-radio-player A terminal based radio interface for linux.

License

BSD 3-Clause License

Copyright (c) 2019-2021, Jakub Wlodek

py_cui's People

Contributors

aaronpierce avatar channel-42 avatar hakiergrzonzo avatar imgbot[bot] avatar jupiterbjy avatar jwlodek avatar mohan-cloud avatar ne-msft avatar pablolec avatar readmodifywrite avatar telday avatar vamporelol avatar voryzen avatar wdog avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

py_cui's Issues

Improve testing infrastructure

Is your feature request related to a problem? Please describe.
The current testing solution creates instances of each object for each test case. This is redundant and can be simplified

Describe the solution you'd like
Use pytest fixtures to create instances of required objects

set_title does nothing for BlockLabels

Because BlockLabels uses _lines instead of _title, set_title does nothing.

To reproduce:

import py_cui
root = py_cui.PyCUI(2,2)
root.set_title('Block Labels')
label = root.add_block_label('Hello\nWorld', 0,0, row_span=2, column_span=2)
label.set_title('Goodbye\nWorld')
root.start()

Expected behavior
set_title(title) should set the BlockLabel's ._lines attribute to title.splitlines(), or an alternative function should be supplied to set the lines attribute directly.

Environment:

  • OS: Windows 10
  • Terminal: Powershell Core
  • Version: 0.1.1

Add border color selection, improve widget coloring options.

Colors need to be expanded to allow for more complex configurations, and border color should be a simple setting

Describe the solution you'd like
Border color attribute and function. Defaults to standard widget color.

Additional coloration options to allow finer control of widget coloring. Color rules will remain top level coloration option, but if lower level one is assigned it will take precedence (ex - color selected item in scroll menu)

Request: Slider

Hi I'm new to python and I'm very happy to use your library as a learning tool

I'm currently writing a very simple app https://github.com/wdog/mini-radio-player-pycui using your library.

I've implemented a simple Slider and a few others things as a updatable Labels.

I think that would be useful if you implement some of this directly in your library.

I know that my code is awful and very buggy, but there are some good ideas (I hope) 😄

Thanks

BlockLabel: wrong center text

I think I've found a little bug.

The parameter center=True|False is not working because in the method add_block_label in __init__.py when creating the widget LabelBlock I've found the last 2 parameter inverted (logger and center)

fix

new_label   = widgets.BlockLabel(id,  ..., center,  self._logger)

Update Element with Thread

I've this little examples that does not work... It' doesn't update the screen until I press a key.

Basically what I want is to run a thread that updates some element and update the screen
Can you help me to understand why it is not completely working?

#!/usr/bin/env python3

import py_cui
import threading
import time
from datetime import datetime


class App:
    def __init__(self, master):
        self.master = master
        self.g1 = self.master.add_scroll_menu('PANEL',
                                              0, 0, row_span=6, column_span=6)
        thread = threading.Thread(target=self.thread_update, args=())
        thread.daemon = True
        thread.start()

    def thread_update(self):
        try:
            while True:
                d = "TIME:{}".format(datetime.now().strftime('%X'))
                self.g1.add_item(d)
                time.sleep(1)
        except Exception as e:
            print(e)


root = py_cui.PyCUI(9,6)
root.toggle_unicode_borders()
root.set_title('Test')
s = App(root)
root.start()

Is it a thread/curses limitation?

Add dialog popups and widgets

Add additional folder dialog_widgets, and include widgets and popups for selecting file/directory, forms, etc.

  • Forms
  • File select
  • Directory select

Logger Implementation

It would make the debugging process significantly easier if there was some type of logging ability. I would propose an INFO level logger, which would output to a file and then could be used to diagnose user issues, as well as a DEBUG level logger which could either output to the status bar or somewhere else on the screen while the TUI is still being drawn. The best resource for this would be pythons builtin logging module

This will be next on my list if someone else doesn't implement it first.

Cannot move to next column if widget is not on row 0

Getting an issue where I cannot move to the next column if there is not an item on the first row of the next column. The code im using as follows (with offending imports commented out.):

import py_cui
#import pantrack

class Interface():

	def __init__(self):
		#self.db = pantrack.Database()
		self.master = py_cui.PyCUI(6,2)
		self.task_menu = self.master.add_scroll_menu('Tasks', 0, 0, row_span=6)
		self.update_button = self.master.add_button('Update', 1, 1, command=self.update_tasks)

	def start(self):
		self.master.start()

	def update_tasks(self):
		#tasks = [task['short_desc'] for task in self.db.get_tasks()]
		self.task_menu.add_item_list(tasks)

if __name__ == '__main__':
        ui = Interface()
        ui.start()

The error still persisted when i tried:

  • Different types of widgets
  • The first column widget didnt use the max row_span
  • The row of the first column widget was greater than 0.

Popup multiline

@jwlodek what do you think about implementing multiline popup

basically I think is enough to change _draw_ method on Popup class (popyp.py)
with something like this.

I need something like that for implementing popups with helps or descriptions (readonly multiline popup text)

    def _draw(self):
        """Function that uses renderer to draw the popup

        Can be implemented by subclass. Base draw function will draw the title and text in a bordered box
        """

        super()._draw()

        self._text_lines = self._text.splitlines()
        if len(self._text_lines) == 0:
            self._text_lines.append('')

        # limit to 5 row of text 
        self._text_lines = self._text_lines[0:5]

        target_y = int(self._stop_y - ( self._start_y + len(self._text_lines))// 2)
        self._renderer.set_color_rules([])
        self._renderer._set_bold()
        self._renderer.set_color_mode(self._color)
        self._renderer.draw_border(self, with_title=False)
        self._renderer.draw_text(self, self._title, target_y - 2,
                                 centered=True, selected=True)
        for line in self._text_lines:
            self._renderer.draw_text(self, line,  target_y,
                                    centered=True, selected=True)
            target_y += 1
        self._renderer.unset_color_mode(self._color)
        self._renderer._unset_bold()
        self._renderer.reset_cursor(self)

Maybe passing Centered as parameter, changing size of the popup to fit all the text or at least more rows (I've limited to 5 line in the code) and positiong text at the top of the popup.

Is seems that changing the code will not break existing popups...

Base class for Container widget

Is your feature request related to a problem? Please describe.
Several widgets and popups are easiest to construct using a combination of other widgets. For example, the form popup or file popup.

Describe the solution you'd like
A set of classes should be made describing a Container widget. This should eliminate boilerplate required for such widget combinations.

Additional context
Form and FileDialog popups likely good candidates to work from

Border colors don't get set correctly when widget is in focus if not specified

Describe the bug
When the focus border color is not explicitly set, it defaults to widget color, instead of border color.

To Reproduce
Steps to reproduce the behavior:

  1. Set widget border color to non default value
  2. Select widget

Expected behavior
Focus border color should retain standard border color if not directly specified

Screenshots
In the below image the warning popup is broken since the focus border color defaults to WHITE_ON_BLACK despite the border color being set to YELLOW_ON_BLACK
image

Environment:

  • OS: N/A
  • Terminal: N/A
  • Version: v0.1.2

Set color mode issue.

I got a strange error while working with py-cui. When I execute my code everything looks great but when I press enter on one of my buttons it gives me this error. Any ideas?

File "/usr/local/lib/python3.7/dist-packages/py_cui/widgets.py", line 381, in draw self.renderer.set_color_mode(self.color) AttributeError: 'NoneType' object has no attribute 'set_color_mode'

Block Label's method set_title() doesn't change the title in the widget

Describe the bug
Method set_title() of block label doesn't change the title in the widget, but internally in the variables it does. You can know this because when you get the value of the title, it returns you the new value not the previous one.

To Reproduce
It is shown in the screenshoot of the code editor

Expected behavior
It is expected that when the method set_title() is called, it should change the title of the widget block label as it does with label

Screenshots
imagen
imagen
imagen

Environment:

  • OS: Microsoft Windows [Versión 10.0.18362.900]
  • Terminal: CMD, Windows Terminal (WSL - Ubuntu 20.04)

Add function for selecting background color

Is your feature request related to a problem? Please describe.
Background color should be easily select-able, so TUIs with different styles can be more easily developed.

Describe the solution you'd like
A method in the base PyCUI class that takes a single background color paramter

Create base classes for text line, scroll cell

There is lots of redundant code between popups and widgets that does the same thing. A new set classes should be created, and the respective widgets/popups should extend these in addition to the base Widget and Popup classes.

Allow user to provide custom function to generate a line in ScrollMenu

Allow user to specify a custom to_str function for the ScrollMenu (and other similar widgets) that would be used to determine the line of text to display on the screen. Currently, only strings are allowed to be added to the widget.

Motivation? Imagine we had a list of Pet objects that might be defined as:

class Pet:
  def __init__(self, name, breed, age):
    self.name = name
    self.breed = breed
    self.age = age

  def purchase(self):
    print(f"You purchased a {self}")

  def __str__(self):
    return f"name={self.name} breed={self.breed} age={self.age}"

To display a list of pets and add an event handler for one that is selected in a ScrollMenu, I'd have to do the following:

pets = [Pet("Baloo", "Dog", 4), Pet("Wilbur", "Horse", 7)]
menu = root.add_scroll_menu("Pets", 0, 0)
menu.add_item_list(str(p) for p in pets) # explicitly convert to string

def process_selection():
  selected_pet_text = menu.get()
  for p in pets:
    if str(p) == selected_pet_text:
      p.purchase()

menu.add_key_command(py_cui.keys.KEY_ENTER, process_selection)

With a very small change to the code, users wouldn't have to look up which Pet was selected because the return value from ScrollMenu.get now returns the actual Pet object itself:

pets = [Pet("Baloo", "Dog", 4), Pet("Wilbur", "Horse", 7)]
menu = root.add_scroll_menu("Pets", 0, 0)
menu.add_item_list(pets) # Allow any object to be added

def process_selection():
  selected_pet_object = menu.get() # this returns the object in the list
  selected_pet_object.purchase()   # so we can use it directly

menu.add_key_command(py_cui.keys.KEY_ENTER, process_selection)

Here is a custom MyScrollMenu that I built to add the above, but it the change is so minor and backwards compatible, that I think it should be added to the widget directly. In the custom widget, I've added one new parameter to the constructor: to_str. It has a default value of str. And then in the body of the ScrollMenu.draw method, only one line is changed—the in clause of the for loop:

class MyScrollMenu(py_cui.widgets.ScrollMenu):
    def __init__(self, id, title, grid, row, column, row_span, column_span, padx, pady, to_str=str,
    ):
        super().__init__(id, title, grid, row, column, row_span, column_span, padx, pady)
        self.to_str = to_str

    def draw(self):
        super(py_cui.widgets.ScrollMenu, self).draw()

        self.renderer.set_color_mode(
            self.selected_color if self.selected else self.color
        )
        self.renderer.draw_border(self)

        counter = self.pady + 1
        line_counter = 0
        for line in (self.to_str(i) for i in self.view_items):
            if line_counter < self.top_view:
                line_counter = line_counter + 1
            else:
                if counter >= self.height - self.pady - 1:
                    break
                if line_counter == self.selected_item:
                    self.renderer.draw_text(
                        self, line, self.start_y + counter, selected=True
                    )
                else:
                    self.renderer.draw_text(self, line, self.start_y + counter)
                counter = counter + 1
                line_counter = line_counter + 1

        self.renderer.unset_color_mode(
            self.selected_color if self.selected else self.color
        )
        self.renderer.reset_cursor(self)

Another nice item with this approach is that one can have multiple ScrollMenu widgets all containing the same Pet list, but each could be displayed differently just by passing a custom to_str function:

pets = [Pet("Baloo", "Dog", 4), Pet("Wilbur", "Horse", 7)]

menu1 = root.add_scroll_menu("Pets Detail", 0, 0)
menu1.add_item_list(pets) # Allow any object to be added

menu2 = root.add_scroll_menu("Pet Names", 0, 0, to_str=lambda p: p.name)
menu2.add_item_list(pets) # Allow any object to be added

Youtube tutorial

Should make some youtube tutorial videos on how to write simple things with py_cui

Move CI/CD to github actions

I would like to move the automated unit testing to use github actions instead of Travis. I would also like to add a workflow for automatically generating pypi releases on git tag.

KEY_TAB to switch between widgets and provide focus

For the app I built, I found the Overview and Focus modes a bit unfriendly to use. I have three widgets on my screen as follows.

+--------------------------------------------------------------------------+
|                                     |                                    |
|                                     |                                    |
|                                     |                                    |
|-------------------------------------|                                    |
|                                     |                                    |
|                                     |                                    |
|                                     |                                    |
+--------------------------------------------------------------------------+

Arrow navigation felt a bit wonky in overview mode (as previously reported by a different user), so I wanted to simply make consecutive TAB presses cycle through the three widgets and placing it in focus mode automatically.

So, here is what I did in case anyone else wants to do the same:

        focus = itertools.cycle(root.widgets.values())

        def select_next_widget():
            root.move_focus(next(focus))

        root.add_key_command(py_cui.keys.KEY_TAB, select_next_widget)
        menu1.add_key_command(py_cui.keys.KEY_TAB, select_next_widget)
        menu2.add_key_command(py_cui.keys.KEY_TAB, select_next_widget)
        menu3.add_key_command(py_cui.keys.KEY_TAB, select_next_widget)

Change text of item in scroll menu

This feature request is not related with any problem. It's just something that with the use of the library I notice and I think is missing

The feature I'm telling is the possibilty to change the text of a selected item in a Scroll Menu with a method. This method could receive the index of the item you want to change and the new text, or only recieve the new text and take the value of the index that is inside the class.

Finally, I want to tell you that I really like the way you're developing this library, is really easy to use.

Improve live-debug mode for py_cui

Is your feature request related to a problem? Please describe.
Printing logger messages to status bar is not useful - if multiple log messages are printed before refresh only last one visible

Describe the solution you'd like
Add a default keybinding Ctrl + D if logging is enabled to toggle an overlay window with recent log messages.

Additional context
Can probably re-use a lot of code from popup menu.

Change Color Selected Item

Hi!

how can i change the color of a selected line in a scroll_menu?
(and in general of a particular part of a widget? )

Bold is not enough, What I have to do if I want to use reverse colors GREEN_ON_BLACK and BLACK_ON_GREEN for example.

Can I add an object to the scroll_menu and use maybe __str__ for rendering, so to have a selected object and not a string get() or an int get_selected_item() ?

thanks a lot

AttributeError: 'ScrollMenu' object has no attribute 'add_form'

I created 2 Widgets, scroll menus for each widget.
Python thows AttributeError when add_form() is applied.
I don't know if it's a bug or a misuse.

class mainTUI:

    def __init__(self, master):
        self.master = master
        self.master.set_title("menu example 2")
        self.widget_set_A = self.master.create_new_widget_set(10, 10)
        self.widget_set_B = self.master.create_new_widget_set(10, 10)

        # MENU 1
        self.menu1 = self.widget_set_A.add_scroll_menu('menu_1', 0, 0, row_span=7, column_span=2)
        self.menu1.add_key_command(py_cui.keys.KEY_ENTER, self.toggle_widget_2)
        self.master.apply_widget_set(self.widget_set_A)
        self.menu1.add_form("test")

        # MENU 2
        self.menu2 = self.widget_set_B.add_scroll_menu('menu_2', 0, 0, row_span=7, column_span=2)
        self.menu2.add_key_command(py_cui.keys.KEY_ENTER, self.toggle_widget_1)


    def toggle_widget_1(self):
        self.master.apply_widget_set(self.widget_set_A)

    def toggle_widget_2(self):
        self.master.apply_widget_set(self.widget_set_B)


root = py_cui.PyCUI(10, 10)
root.set_title('CUI TODO List')
s = mainTUI(root)
root.start()

Environment:

  • Debian
  • Terminal: gnome-terminal
  • Version: v3.30.2

A way to destroy/forget widgets

Is your feature request related to a problem? Please describe.
I want to redraw just a small part of the screen I've created. In something like Tk, I would use either .destroy() or pack_forget, etc.

Trying to manually delete items from _widgets causes an error in the draw method.

Describe the solution you'd like
Either attach a destroy method to the overall widget class, or add something like a remove(widget-id) function to the py-cui and widgetset classes

pip install

Just as a suggestion for people confused why they are unable to install with pip, if getting error 'py-cui requires Python '>=3.2' but the running Python is 2.7.17', you must use pip3 to install once cloned.

Add chart widgets

Is your feature request related to a problem? Please describe.
Widgets for charts (line, bar, etc) should be added.

Describe the solution you'd like
A new folder called chart_widgets should be made, and individual chart widgets should be developed and placed there.

  • Bar Chart
  • Line Chart
  • Histogram
  • Pie Chart (Maybe?)
  • Scatterplot

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.