Simple python ui lib for cli programs
Project description
CAT cat_ui
Это легковестный фрэймворк для создания интерактивных CLI программ на питоне.
Установка
Установите напрямую из 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 можно менять стиль отрисовки сторон, углов, а так же внутренности.
Пример.
*--------* 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, Alignmentbox_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)
*----------------------*
| *---* *---*|
| |###| | ||
| *---**---* | ||
| |:::| | ||
| *---**---*| ||
| | || ||
| *---**---*|
*----------------------*
App
До сих пор отрисовка всех контейнеров происходила в print. Однако для более сложных взаимодействий, таких как : изменение окон, ввод текста с клавиатуры - есть отдельный класс App.
width, height - высота и ширина приложения, соответственно.
set_screen(window: ContainerElement) устанавливает текущее окно.
stop останавливает приложение.
Метод run является асинхронным, поэтому запускать его необходимо с помощью asyncio:
import asyncio
asyncio.run(app.run())
либо же внутри другого асинхронного метода.
Сам по себе метод run представляет цикл, в котором отрисовываются все контейнеры, а так же отрабатывают все нажатия на клавиши.
Пример запуска.
from cat_ui import Box, Alignment, App
import asyncio
app = App(20, 10, Box(10, 5, alignment=Alignment.BOTTOM_RIGHT))
asyncio.run(app.run())
Пример смены экранов.
from cat_ui import VerticalContainer, Button, Label, Alignment, App
import asyncio
app = App()
window_1 = VerticalContainer(alignment=Alignment.TOP_CENTER)
window_2 = VerticalContainer(alignment=Alignment.TOP_CENTER)
button = Button("next window", alignment=Alignment.BOTTOM_CENTER)
button.add_action(lambda btn: app.set_screen(window_2))
window_1.append(Label("This is window 1"))
window_1.append(button)
window_2.append(Label("This is window 2"))
app.set_screen(window_1)
asyncio.run(app.run())
Пример закрытия приложения.
from cat_ui import VerticalContainer, Label, Button, Alignment, App
import asyncio
app = App()
window = VerticalContainer(min_width=10, left_padding=2,right_padding=2, alignment=Alignment.TOP_CENTER)
window.append(Label("Pressing this button will terminate window"))
button = Button("click me", alignment=Alignment.BOTTOM_CENTER)
button.add_action(lambda btn: app.stop())
window.append(button)
app.set_screen(window)
asyncio.run(app.run())
Кнопки и чекбоксы
Button
Кнопка представляет собой BoxContainer с Label внутри. При выборе кнопки, меняется стиль отрисовки контейнера.
style - обычный стиль отрисовки кнопки.
selected_style - стиль при выделении.
При нажатии Enter(↵) срабатывают все функции, которые были привязаны к кнопке.
add_action(action) позволяет привязать новое действие.
Функции, на которые подкисывается кнопка имеют вид
action(button: Button).
Пример.
from cat_ui import VerticalContainer, HorizontalList, Label, Button, styles, Alignment, App
import asyncio
app = App()
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)
app.set_screen(window)
asyncio.run(app.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, App
import asyncio
app = App()
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()}"))
app.set_screen(window)
asyncio.run(app.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, App import asyncio
app = App()
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)
app.set_screen(window) asyncio.run(app.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, App
import asyncio
app = App()
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)
app.set_screen(window)
asyncio.run(app.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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cat_ui-1.1.1.tar.gz.
File metadata
- Download URL: cat_ui-1.1.1.tar.gz
- Upload date:
- Size: 20.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
699ad6c7075d468f7ab4c94455ce3c6c26c8e30eb41d51098fe4bc40b29a9d6e
|
|
| MD5 |
e7cfedf2ea164a9cb20dfdc6565c6804
|
|
| BLAKE2b-256 |
5ce49cbbf28087c48e610a4b3b90c09e5684f811e507d6622a3d69c404b9f376
|
File details
Details for the file cat_ui-1.1.1-py3-none-any.whl.
File metadata
- Download URL: cat_ui-1.1.1-py3-none-any.whl
- Upload date:
- Size: 16.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.16
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc111fe5e08ad26f2def050a950e229b39b30543010cb5526e3a7df10ed06596
|
|
| MD5 |
9ae2658c96ff7d27ad567feb3b46ed0c
|
|
| BLAKE2b-256 |
561c589a00f889b1652d33bacca64e83894419dada38e6c91ac7a59e4766879f
|