Создаем TUI на python

  • Tutorial

Привет, Хабр! В этой статье я расскажу про npyscreen — библиотеке для создания текстовых интерфейсов для терминальных и консольных приложений.




Установка


Пакет доступен для скачивания через PyPI.


sudo pip3 install npyscreen

Типы объектов


Npyscreen использует 3 основных типа объектов:


  • Application objects — обеспечивают запуск и завершение приложения, создание форм, обработку событий.
    В основном используются NPSAppManaged и StandardApp(с поддержкой событий).
  • Form objects — область экрана, которая содержит виджеты.
    Основные формы:
    • FormBaseNew — пустая форма.
    • Form — форма с кнопокой «ok».
    • ActionForm — форма с двумя кнопками: «ok» и «cancel».
    • FormWithMenus — форма, поддерживающая работу с меню.
  • Widget Objects — различные элементы, расположенные на формах.
    Некоторые виджеты:
    • Textfield, PasswordEntry, MultiLineEdit, FilenameCombo* — формы для ввода данных.
    • DateCombo, ComboBox, FilenameCombo — выпадающие списки.
    • MultiSelect, MultiSelect, BufferPager — виджеты с возможностью выбора.
    • Slider, TitleSlider — слайдеры.

Больше информации можно найти на официальном сайте с документацией.


Напишем Hello World


Формы удобно создавать, наследуя их от встроенных классов. Таким образом, можно переопределить встроенные методы для расширения функционала приложения.


Вот так выглядит простой Hello World
#!/usr/bin/env python3
import npyscreen

class App(npyscreen.StandardApp):
    def onStart(self):
        self.addForm("MAIN", MainForm, name="Hello Habr!")

class MainForm(npyscreen.ActionForm):
    # Конструктор
    def create(self):
        # Добавляем виджет TitleText на форму
        self.title = self.add(npyscreen.TitleText, name="TitleText", value="Hello World!")
    # переопределенный метод, срабатывающий при нажатии на кнопку «ok»
    def on_ok(self):
        self.parentApp.setNextForm(None)
    # переопределенный метод, срабатывающий при нажатии на кнопку «cancel»
    def on_cancel(self):
        self.title.value = "Hello World!"

MyApp = App()
MyApp.run()



Расположение элементов


По умолчанию виджеты занимают максимально возможное пространство.
Чтобы задать точные координаты, нужно задать параметры:


  • relx, rely — позиция виджета относительно начала координат формы.
  • width, height, max_width, max_height — ограничения размеров виджета.

Пример
#!/usr/bin/env python3
import npyscreen

class App(npyscreen.StandardApp):
    def onStart(self):
        self.addForm("MAIN", MainForm, name="Hello Habr!")

class MainForm(npyscreen.FormBaseNew):
    def create(self):
        # Узнаем используемое формой пространство
        y, x = self.useable_space()
        self.add(npyscreen.TitleDateCombo, name="Date:", max_width=x // 2)
        self.add(npyscreen.TitleMultiSelect, relx=x // 2 + 1, rely=2, value=[1, 2], name="Pick Several", values=["Option1", "Option2", "Option3"], scroll_exit=True)
        # Можно использовать отицательные координаты
        self.add(npyscreen.TitleFilename, name="Filename:", rely=-5)

MyApp = App()
MyApp.run()



Боксы и пользовательские цвета


Сделать обертку в виде бокса просто — нужно создать класс, наследованный от BoxTitle и переопределить атрибут _contained_widget, положив туда виджет, который будет находиться внутри.
В npyscreen доступно несколько встроенных цветовых тем. При желании можно добавить свои. Установить их можно с помощью метода setTheme.
С настройкой цвета текста все немного сложнее. Мне пришлось расширить функционал библиотеки, чтобы это работало.


Пример
#!/usr/bin/env python3
from src import npyscreen
import random

class App(npyscreen.StandardApp):
    def onStart(self):
        # Устанавливаем тему. По умолчанию используется DefaultTheme
        npyscreen.setTheme(npyscreen.Themes.ColorfulTheme)
        self.addForm("MAIN", MainForm, name="Hello Habr!")

class InputBox(npyscreen.BoxTitle):
    # MultiLineEdit теперь будет окружен боксом
    _contained_widget = npyscreen.MultiLineEdit

class MainForm(npyscreen.FormBaseNew):
    def create(self):
        y, x = self.useable_space()
        obj = self.add(npyscreen.BoxTitle, name="BoxTitle", 
              custom_highlighting=True, values=["first line", "second line"], 
              rely=y // 4, max_width=x // 2 - 5, max_height=y // 2)
        self.add(InputBox, name="Boxed MultiLineEdit", footer="footer", 
              relx=x // 2, rely=2)

        color1 = self.theme_manager.findPair(self, 'GOOD')
        color2 = self.theme_manager.findPair(self, 'WARNING')
        color3 = self.theme_manager.findPair(self, 'NO_EDIT')

        color_list = [color1, color2, color3]
        first_line_colors = [random.choice(color_list) for i in range(len("first line"))]
        second_line_colors = [random.choice(color_list) for i in range(len("second"))]
        # Заполняем строки кастомными цветами
        obj.entry_widget.highlighting_arr_color_data = [first_line_colors, second_line_colors]

MyApp = App()
MyApp.run()



События и обработчики


Класс StandardApp в npyscreen поддерживает очередь событий.
В качестве обработки нажатий используется метод add_handlers.


Пример
#!/usr/bin/env python3
import npyscreen
import curses

class App(npyscreen.StandardApp):
    def onStart(self):
        self.addForm("MAIN", MainForm, name="Hello Habr!")

class InputBox1(npyscreen.BoxTitle):
    _contained_widget = npyscreen.MultiLineEdit
    def when_value_edited(self):
        self.parent.parentApp.queue_event(npyscreen.Event("event_value_edited"))

class InputBox2(npyscreen.BoxTitle):
    _contained_widget = npyscreen.MultiLineEdit

class MainForm(npyscreen.FormBaseNew):
    def create(self):
        self.add_event_hander("event_value_edited", self.event_value_edited)
        new_handlers = {
            # Устанавливаем ctrl+Q для выхода
            "^Q": self.exit_func,
            # Устанавливаем alt+enter для очистки inputbox
            curses.ascii.alt(curses.ascii.NL): self.inputbox_clear
        }
        self.add_handlers(new_handlers)

        y, x = self.useable_space()
        self.InputBox1 = self.add(InputBox1, name="Editable", max_height=y // 2)
        self.InputBox2 = self.add(InputBox2, footer="No editable", editable=False)

    def event_value_edited(self, event):
        self.InputBox2.value = self.InputBox1.value
        self.InputBox2.display()

    def inputbox_clear(self, _input):
        self.InputBox1.value = self.InputBox2.value = ""
        self.InputBox1.display()
        self.InputBox2.display()

    def exit_func(self, _input):
        exit(0)

MyApp = App()
MyApp.run()



Ссылки:


Официальная документация
Оригинальные исходные коды
Обновленный мною репозиторий (основной гитхаб, кажется, умер)
Телеграм клиент на npyscreen (который на первом скриншоте)

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 40

    +9

    Pascal… TurboVision вспомнилось… Как давно это было.

      +2
      Ат опередили, а (:
        0

        Зашел, чтобы написать этот комментарий

          0
          Не могу удержаться от некрокоммента:

          Почему было? Оно живое :) github.com/magiblot/tvision

          image
          0
          Эх, а я как раз начал писать что-то подобное, только сразу для терминала и для веба, этакое два-в-одном. Теперь есть, откуда черпать вдохновение.
            +6
            А мне BOOTSTRA.386 вспомнилась, тема для Bootstrap.

            p.s. кликайте на Demo — там даже построчная отрисовка курсором имитируется, офигеть!
            • UFO just landed and posted this here
                0
                Аж слезу ностальгии выбило, спасибо :))
                0
                Интересно, люди просто регулярно пишут такие либы или оно нынче в тренде? Потому что я как раз пописываю подобную обёртку вокруг bearlibterminal, который рисует ASCII-интерфейс на SDL. Смысл в том, чтоб выглядело одинаково независимо от терминала пользователя, для геймдева.
                  0

                  Да нет, просто я только-только встаю на путь разработчика и пока что просто пишу всякое ради получения опыта.

                0
                Как можно сравнивать прекрасный TV и это?
                  0
                  Что есть TV?
                  А гугл по запросу «python TV» выдает явно не то.
                    +1
                    Pascal… TurboVision
                      0
                      Очевидно, TurboVision из первого коммента. Библиотека UI для Turbo Pascal. Середина 90-х, ностальгия.
                        0
                        Не соотнес, что это ответ к первому комменту
                    +3

                    Когда плюсуешь статью, только увидев картинку в топике. Круть

                      0
                      Чего только люди не придумают, чтобы не делать import curses…
                        0
                        А, это обёртка над curses. Вы бы уточнили это в статье.
                        0
                        Я было понадеялся что прям с нуля.
                          0
                          Что именно с нуля?
                            0
                            Собственный tui-фреймворк с разными фишками и настройками цвета.
                          +3

                          Конечно, прожженных и умудренных опытом людей таким не удивить, но все же…
                          Однажды, в качестве лабораторной работы по каким-то там сетям надо было сделать чат. Пока мои одногруппники штурмовали Qt, я извратился и написал такое:


                          Заголовок спойлера


                          Правда, не совсем честно — можно было бы все кодами сделать, но несколько WinAPI функций присутствовали.

                            0
                            Если всё сделать кодами, то оно только на Windows10 работать будет…
                              0
                              Куда смотреть исходники не слишком опытным windows-программистам?
                                +1

                                Вот оно. Хотя, признаюсь, тот еще говнокод. Особенно, ввод текста.

                                  0
                                  А можно попросить добавить лицензию?
                                    +1
                                    Добавил. Если Вас удивит такой выбор лицензии — работа в значительной мере была сделана как шутка, да и примером «как надо делать консольный GUI» не является. Да и мне абсолютно не жалко любые использования. Если кому-то поможет — только рад.
                              0
                              Что-то с русским языком не подружился приведенный телеграм клиент.
                              Библиотека приятная, ламповая. Спасибо.
                                +1
                                Прошу прощения, вс1 с русским там нормально. «Сам дурак» :)
                                Еще раз спасибо. Будем изучать.
                                  0
                                  А можно подробнее, пожалуйста?
                                  Установил по инструкции с github, на Kubuntu 16.04.4 — terminator / konsole и после запуска и логина отображается только латиница.
                                0
                                Большое спасибо за пост, прямо то, что доктор прописал.

                                С Python я только еще начинаю дружить, зато TUI использую часто: удобно создавать приложения, состоящие практически только из меню и логов…
                                  0
                                  Кто-нибудь встречал такое-же, но под .net?
                                  0
                                  Кстати интересно. Поставил Python 3.6.5 для Windows x86_64, и вот такой баг ловлю при попытке установки npyscreen…
                                  Посмотреть снимок экрана
                                  image
                                    0
                                    Насколько я понял эта библиотека обертка над стандартной curses.
                                    Нужно поставить сперва ее, я ее поставил из whl
                                    После этого установилась npyscreen
                                    Но у меня не заработало, и вроде как на stackoverflow пишут что либа только для posix систем.

                                    У меня вот такая ошибка при запуске приложения

                                    Traceback (most recent call last):
                                    File «C:\proj\reps\python_post_request\curses_test.py», line 15, in wrapper(main)
                                    File «C:\Users\****\AppData\Local\Programs\Python\Python36-32\lib\curses\__init__.py», line 73, in wrapper
                                    stdscr = initscr()
                                    File «C:\Users\****\AppData\Local\Programs\Python\Python36-32\lib\curses\__init__.py», line 30, in initscr
                                    fd=_sys.__stdout__.fileno())
                                    AttributeError: 'NoneType' object has no attribute 'fileno'
                                    +1
                                    Спасибо, интересная статья! Как раз искал подобное решение.
                                      0
                                      А я правильно понимаю, что данная библиотека уж сто лет как заброшенная? Или я куда-то не туда смотрю?
                                        0
                                        По-видимому, да. Но Вы можете самостоятельно пропатчить библиотеку под свои нужды.
                                        Также Вы можете найти больше информации в goolge groups
                                        0
                                        Использую class App(npyscreen.NPSAppManaged) и value_changed_callback=self.actionPress у кнопки,
                                        первое нажатие отрабатывает хорошо, на последующие не реагирует, как вернуть объект к первоначальному состоянию?
                                        def actionPress(self, widget):
                                        if self.actionButton.value == True:
                                        selected = self.Options.get_selected_objects()
                                        if selected[0] ==

                                        Only users with full accounts can post comments. Log in, please.