Skip to main content

Simple python ui lib for cli programs

Project description

CAT cat_ui

GitHub PyPI

Это легковестный фрэймворк для создания интерактивных CLI программ на питоне.

preview

Установка

Установите напрямую из PyPI:

pip install cat-ui

Или локально из исходников:

git clone https://github.com/LazzyCatto/cat_ui.git
cd cat_ui
pip install .

Запуск

Чтобы быстро попробовать cat_ui, создайте файл main.py со следующим содержимым:

from cat_ui import Label, run, set_screen
import asyncio

set_screen(Label("Hello cat_ui!"))
asyncio.run(run())

Запустить можно с помощью:

python main.py

Чтобы выйти нажмите Ctrl+C.

Документация

Контейнеры

ContainerElement

Базовым класом всех элементов является ContainerElement. Именно он отвечает за обработку клавиатуры, а так же отрисовку.

get_width, get_height возвращают ширину и высоту контейнера соответственно (если элемент неправильной формы, то get_width, get_height должны возвращать длину и высоту описаного прямоугольника). Они отвечают за изменение размеров родительских контейнеров.

draw как раз и отвечает за отрисовку, он возвращает строчку, которая в дальнейшем будет печататься. У всех ContainerElement гарантируется, что курсор начинает и заканчивает отрисовку в левом-верхнем углу контейнера.

draw_on_screen применяет alignment для размещения контейнера. Работает соглашения draw: курсор начинает и заканчивает свою работу в левом-верхнем краю экрана.

process_key принимает строку кнопки, обрабатывает взаимодействие, а после возвращает кнопку, которая будет обрабатываться родительским контейнером. Если родительский контейнер не должен ничего обрабатывать - возвращается "".

selectable возвращает True, если с контейнером можно взаимодействовать и False иначе.

select, deselect методы отвечают за подсветку или иное обозначение выбраного контейнера (например Button меняет стиль своей границы, а InputField делает текст жирым).

Box

Контейнер, который отрисовывается как прямоугольник. У Box можно менять стиль отрисовки сторон, углов, а так же внутренности.

Пример.
from cat_ui import HorizontalList, Box, styles

list_of_boxes = HorizontalList()  # этот элемент нужен для отображения всех `Box`

box_1 = Box(width=10, height=5)
box_2 = Box(width=10, height=5, vertical_bar="$", horizontal_bar="~", corners=["1", "2", "3", "4"], fill="@")
box_3 = Box(width=10, height=5, **styles.pretty)
box_4 = Box(width=10, height=5, **styles.invisible)
box_5 = Box(width=10, height=5, **styles.bold_pretty)

list_of_boxes.append(box_1)
list_of_boxes.append(box_2)
list_of_boxes.append(box_3)
list_of_boxes.append(box_4)
list_of_boxes.append(box_5)

print(list_of_boxes.draw(), end="", flush=True)
*--------* 1~~~~~~~~2 ┌────────┐            ┏━━━━━━━━┓
|        | $@@@@@@@@$ │        │            ┃        ┃
|        | $@@@@@@@@$ │        │            ┃        ┃
|        | $@@@@@@@@$ │        │            ┃        ┃
*--------* 3~~~~~~~~4 └────────┘            ┗━━━━━━━━┛

Label

Тестовый контейнер. Текст может быть ограничен по ширине : тогда нужно выставить соответствующее значение width.

get_text возвращвет текущий текст.

set_text(text, [width]) устанавливает новый текст.

Если width не установлен, текст будет не ограничен по ширине

Пример.
from cat_ui import VerticalList, Label, Alignment

labels = VerticalList(min_width=40) # этот элемент нужен для отображения всех `Label`

label_1 = Label("label_1")
label_2 = Label("lb_2", alignment=Alignment.CENTER_RIGHT)
label_3 = Label("This label will be restricted by width", width=10, alignment=Alignment.CENTER)

labels.append(label_1)
labels.append(label_2)
labels.append(label_3)

print(labels.draw(), end="", flush=True)
label_1

                                    lb_2

               This label
                will be r
               estricted 
               by width

BoxContainer

Это Контейнер, который автоматически увеличивается, под размер содержимого. Внутри все вложеные контейнеры расположены свободно, тоесть их положение определяется их alignment.


внутренние контейнеры выбираются стрелками:

  • ( ← ) / ( ↑ ) для выбора предыдущего,
  • ( → ) / ( ↓ ) для выбора следующего.

В случае, если внутренние контейнеры перекрываются, выше будет тот, который добавлен позже.

min_width, min_height - минимальные размеры контейнера.

top_padding, left_padding, right_padding, bottom_padding - отступы от краев контейнера (по умолчанию 1).

append добавляет контейнер внутрь.

Пример. ```python from cat_ui import BoxContainer, Box, Alignment

box_container = BoxContainer(min_width=40, min_height=2, bottom_padding=2, right_padding=4)

box = Box(width=5, height=7, alignment=Alignment.CENTER_RIGHT, fill="#")

box_container.append(box)

print(box_container.draw(), end="", flush=True)


-------------------------------------- | --- | | |###| | | |###| | | |###| | | |###| | | |###| | | --- | | | --------------------------------------

</details>

#### VerticalList/HorizontalList

Увеличивающийся контейнер, который распологает внутри элементы по верликали (`VerticalList`) сверху вниз или по горизонтали (`HorizontalList`) слева направо.

внутренние контейнеры выбираются стрелками:
- ( ← ) - `HorizontalList` / ( ↑ ) - `VerticalList` для выбора предыдущего,
- ( → ) - `HorizontalList` / ( ↓ ) - `VerticalList` для выбора следующего.

`min_width`, `min_height` - минимальные размеры контейнера.

`space` - растояние между элементами (по умолчанию 1).

<details><summary>Пример.</summary>

```python
from cat_ui import HorizontalList, Box, Alignment

horizontal_list = HorizontalList(space=0)

box_1 = Box(width=5, height=3, fill="#")
box_2 = Box(width=5, height=3, alignment=Alignment.CENTER_RIGHT, fill=":")
box_3 = Box(width=5, height=3, alignment=Alignment.BOTTOM_LEFT)
box_4 = Box(width=5, height=7, alignment=Alignment.BOTTOM_LEFT)

horizontal_list.append(box_1)
horizontal_list.append(box_2)
horizontal_list.append(box_3)
horizontal_list.append(box_4)

print(horizontal_list.draw(), end="", flush=True)
*---*          *---*
|###|          |   |
*---**---*     |   |
     |:::|     |   |
     *---**---*|   |
          |   ||   |
          *---**---*

VerticalContainer/HorizontalContainer

То же самое, что и VerticalList/HorizontalList и BoxContainer одновременно.

Пример.
from cat_ui import HorizontalContainer, Box, Alignment

horizontal_container = HorizontalContainer(space=0, left_padding=3)

box_1 = Box(width=5, height=3, fill="#")
box_2 = Box(width=5, height=3, alignment=Alignment.CENTER_RIGHT, fill=":")
box_3 = Box(width=5, height=3, alignment=Alignment.BOTTOM_LEFT)
box_4 = Box(width=5, height=7, alignment=Alignment.BOTTOM_LEFT)

horizontal_container.append(box_1)
horizontal_container.append(box_2)
horizontal_container.append(box_3)
horizontal_container.append(box_4)

print(horizontal_container.draw(), end="", flush=True)
*----------------------*
|  *---*          *---*|
|  |###|          |   ||
|  *---**---*     |   ||
|       |:::|     |   ||
|       *---**---*|   ||
|            |   ||   ||
|            *---**---*|
*----------------------*

Кнопки и чекбоксы

Button

Кнопка представляет собой BoxContainer с Label внутри. При выборе кнопки, меняется стиль отрисовки контейнера.

style - обычный стиль отрисовки кнопки.

selected_style - стиль при выделении.


При нажатии Enter(↵) срабатывают все функции, которые были привязаны к кнопке.

add_action(action) позволяет привязать новое действие.

Функции, на которые подкисывается кнопка имеют вид action(button: Button).


Пример.
from cat_ui import VerticalContainer, HorizontalList, Label, Button, styles, Alignment, run, set_screen
import asyncio

window = VerticalContainer(**styles.pretty, alignment=Alignment.TOP_CENTER)
output = Label("no buttons have been pressed.", alignment=Alignment.CENTER)
window.append(output)

def change_output(button: Button):
    output.set_text(f"\"{button.children[0].get_text()}\" has been pressed!")

button_1 = Button("button 1", style=styles.pretty, selected_style=styles.bold_pretty)
button_2 = Button("button 2", style=styles.pretty, selected_style=styles.bold_pretty)
button_3 = Button("button 3", style=styles.pretty, selected_style=styles.bold_pretty)

button_1.add_action(change_output)
button_2.add_action(change_output)
button_3.add_action(change_output)

button_row = HorizontalList()
window.append(button_row)

button_row.append(button_1)
button_row.append(button_2)
button_row.append(button_3)

set_screen(window)
asyncio.run(run())
                                 ┌────────────────────────────────┐
                                 │  "button 3" has been pressed!  │
                                 │                                │
                                 │┌────────┐ ┌────────┐ ┏━━━━━━━━┓│
                                 ││button 1│ │button 2│ ┃button 3┃│
                                 │└────────┘ └────────┘ ┗━━━━━━━━┛│
                                 └────────────────────────────────┘

SelectorLabel : Checkbox и Radio

SelectorLabel представляет из себя Label, который работает как кнопка, на которую можно нажимать с помощью Enter(↵).

chosen_prefix это префикс обозначающий, что данный SelectorLabel был выбран.

plain_prefix это префикс обозначающий, что данный SelectorLabel не был выбран.

На соответствующие действия можно подписываться.

add_action_on(action) - action будет выполнен, как только SelectorLabel стал выбран.

add_action_off(action) - action будет выполнен, как только SelectorLabel перестал быть выбран.

Так же как и у Button, функция, на которые подписывается SelectorLabel имеет вид action(button: Button).

По отдельности он мало представляет интерес, поэтому далее рассмотрим классы VerticalCheckbox, HorizontalCheckbox, VerticalRadio, HorizontalRadio.


VerticalCheckbox/HorizontalCheckbox представляют из себя VerticalList/HorizontalList состоящий из SelectorLabel.

get_mask позволяет получить массив bool, показывающий текущее состояние SelectorLabel.

get_indices возвращвет массив индексов выбраных элементов.

Можно так же привязываться к изменению состояния VerticalCheckbox/HorizontalCheckbox с помощью add_action(action).

``

Пример.
from cat_ui import VerticalContainer, VerticalCheckbox, Button, Label, styles, Alignment, run, set_screen
import asyncio

window = VerticalContainer(
    space=2,
    left_padding=2,
    right_padding=2,
    alignment=Alignment.TOP_CENTER,
    **styles.pretty
    )
window.append(Label("THE ULTIMATE TODO LIST", alignment=Alignment.TOP_CENTER))
label = Label("")

plan_list = VerticalCheckbox(
    [
    "make cool application with cat_ui",
    "do a backflip",
    "go to sleep",
    "watch anime 24/7"
    ], chosen_prefix="🗹 ", plain_prefix="☐ ")
window.append(plan_list)

plan_list.children[-1].add_action_on(lambda sl: label.set_text("toch some grass"))
plan_list.children[-1].add_action_off(lambda sl: label.set_text("achivement unlocked: \"toch some grass\""))

done_button = Button("DONE", min_width=10, alignment=Alignment.BOTTOM_CENTER, style=styles.pretty, selected_style=styles.bold_pretty)
window.append(done_button)
window.append(label)

done_button.add_action(lambda btn: label.set_text(f"the mask list: {plan_list.get_mask()}"))

set_screen(window)
app = asyncio.run(run())
                           ┌───────────────────────────────────────────┐
                           │          THE ULTIMATE TODO LIST           │
                           │                                           │
                           │                                           │
                           │ 🗹 make cool application with cat_ui       │
                           │                                           │
                           │ ☐ do a backflip                           │
                           │                                           │
                           │ 🗹 go to sleep                             │
                           │                                           │
                           │ ☐ watch anime 24/7                        │
                           │                                           │
                           │                                           │
                           │                ┏━━━━━━━━┓                 │
                           │                ┃  DONE  ┃                 │
                           │                ┗━━━━━━━━┛                 │
                           │                                           │
                           │                                           │
                           │ the mask list: [True, False, True, False] │
                           └───────────────────────────────────────────┘

VerticalRadio/HorizontalRadio - то же, что и VerticalCheckbox/HorizontalCheckbox, только не позволяют нескольким SelectorLabel быть выбраными одновременно.

Вместо get_mask и get_indecies есть метод get_index. Он возвращает индекс выбраного элемента (-1, если не выбрано ничего).

Пример.
from cat_ui import VerticalContainer, VerticalRadio, Label, styles, Alignment, run, set_screen
import asyncio

window = VerticalContainer(
    space=2,
    left_padding=2,
    right_padding=2,
    alignment=Alignment.TOP_CENTER,
    **styles.pretty
    )
window.append(Label("What is your favorite game?"))
label = Label("")

game_list = ["minectart", "terraria", "portal", "the witness"]
game_radio = VerticalRadio(game_list, chosen_prefix="◉ ", plain_prefix="○ ")
window.append(game_radio)

def on_change(radio: VerticalRadio):
    if radio.get_index() == -1:
        label.set_text("")
    else:
        label.set_text(f"I love {game_list[radio.get_index()]} too!")

game_radio.add_action(on_change)

window.append(label)

set_screen(window)
app = asyncio.run(run())
                                  ┌─────────────────────────────┐
                                  │ What is your favorite game? │
                                  │                             │
                                  │                             │
                                  │ ○ minectart                 │
                                  │                             │
                                  │ ○ terraria                  │
                                  │                             │
                                  │ ◉ portal                    │
                                  │                             │
                                  │ ○ the witness               │
                                  │                             │
                                  │                             │
                                  │ I love portal too!          │
                                  └─────────────────────────────┘

Ввод текстовых данных

InputField позволяет вводить текстовые данные. Изначально в текстовом поле записан text.

width - максимальная ширина текста.

set_width(width) выставляет новую максимальную ширину.

можно выставить -1, чтобы текст увеличивался неограничено.

set_text(text) позволяет поменять содержимое текстового поля.

get_text возвращает написаный текст.

alowed_char - допустимый набор символов (по умолчанию это ui.printable).


  • ( ← ) перемещает курсор влево
  • ( → ) перемещает курсор вправо

При нажатии на Enter(↵) срабатывают все функции, которые были привязаны.

add_action(action) позволяет привязать новое действие.

Так же как и у Button, все привязаные функции имеют вид action(button: Button).


PasswordInput - то же, что и InputField, только все символы заменяются на password_char (по умолчанию *).

Пример.
from cat_ui import VerticalContainer, HorizontalList, InputField, PasswordInput, Label, styles, Alignment, run, set_screen
import asyncio

window = VerticalContainer(
    space=2,
    left_padding=2,
    right_padding=2,
    alignment=Alignment.TOP_CENTER,
    **styles.pretty
    )

window.append(Label("What is your name?"))
name_input = InputField()
name_row = HorizontalList(min_height=1)
name_row.append(Label("name:"))
name_row.append(name_input)
window.append(name_row)

greetings_label = Label("")
window.append(greetings_label)

name_input.add_action(lambda name: greetings_label.set_text(f"Hello {name.get_text()}!"))

window.append(Label("Enter secret password (only numbers)"))
password_input = PasswordInput(alowed_char="".join([str(num) for num in range(10)]))
password_row = HorizontalList(min_height=1)
password_row.append(Label("password:"))
password_row.append(password_input)
window.append(password_row)

verify_label = Label("")
window.append(verify_label)

def check_password(password: PasswordInput):
    if password.get_text() == "12345":
        verify_label.set_text("The password is correct!")
    else:
        verify_label.set_text("Wrong password!!!")

password_input.add_action(check_password)

set_screen(window)
app = asyncio.run(run())
                              ┌──────────────────────────────────────┐
                              │ What is your name?                   │
                              │                                      │
                              │                                      │
                              │ name: Akio                           │
                              │                                      │
                              │                                      │
                              │ Hello Akio!                          │
                              │                                      │
                              │                                      │
                              │ Enter secret password (only numbers) │
                              │                                      │
                              │                                      │
                              │ password: *****_                     │
                              │                                      │
                              │                                      │
                              │ The password is correct!             │
                              └──────────────────────────────────────┘

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cat_ui-1.1.0.tar.gz (19.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cat_ui-1.1.0-py3-none-any.whl (16.0 kB view details)

Uploaded Python 3

File details

Details for the file cat_ui-1.1.0.tar.gz.

File metadata

  • Download URL: cat_ui-1.1.0.tar.gz
  • Upload date:
  • Size: 19.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.16

File hashes

Hashes for cat_ui-1.1.0.tar.gz
Algorithm Hash digest
SHA256 3c31b18cdce18d2c7523cc6f794802f1bce39baa8b7348db3908990685b16646
MD5 689b9ecb3a83c5ab0663d36acb7c9bfe
BLAKE2b-256 d2a58e8806a2419cbb469d35eb69581ccde8d8f123ff131c0d47ceec98a1d31c

See more details on using hashes here.

File details

Details for the file cat_ui-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: cat_ui-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 16.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.10.16

File hashes

Hashes for cat_ui-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9a875db34022dd1baf5dfceb1a3e8fc2c2a1a84b3f578c1f1ca6db7b4fabbd84
MD5 f3a29adf1c2181a7689fbb59f1b200a6
BLAKE2b-256 abc2b9fe7d2b118ef5e8ac9a71d1ff9bebaa398c8fcd7b7d66848fd0f6092f0d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page