Создаем 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 (который на первом скриншоте)

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

More
Ads

Comments 39

    +9

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

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

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

        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.