Форум программистов, компьютерный форум, киберфорум
py-thonny
Войти
Регистрация
Восстановить пароль

Создаем CLI приложение на Python с Prompt Toolkit

Запись от py-thonny размещена 13.05.2025 в 10:24
Показов 4648 Комментарии 0
Метки asyncio, cli, python

Нажмите на изображение для увеличения
Название: b06206e8-edff-46d5-bd2c-d084a141cab0.jpg
Просмотров: 42
Размер:	240.5 Кб
ID:	10800
Современные командные интерфейсы давно перестали быть черно-белыми текстовыми программами, которые многие помнят по старым операционным системам. CLI сегодня – это мощные, интуитивные и даже визуально привлекательные инструменты с автодополнением, подсветкой синтаксиса и интерактивными элементами. Если вы когда-нибудь задумывались, как создавать такие продвинутые консольные приложения на Python, погрузитесь в мир Prompt Toolkit – библиотеки, совершившей революцию в разработке командных интерфейсов.

Долгое время разработчики Python использовали стандартный input() или сторонние решения типа readline, которые, мягко говоря, не блистали возможностями. Prompt Toolkit изменил правила игры, предоставив фреймворк для создания по-настоящему современных CLI с богатым пользовательским опытом. Представьте только: автодополнение в реальном времени, полная кастомизация цветов и стилей, продвинутая работа с историей команд, полноценное форматирование текста, поддержка многострочного ввода, асинхронные операции – всё это можно реализовать несколькими строками кода. Инструментарий, который Prompt Toolkit вкладывает в руки разработчика, сравним с тем, что раньше требовало недель программирования низкоуровневых терминальных взаимодействий.

Prompt Toolkit универсален. Он одинаково хорошо подходит как для простых утилит, так и для сложных интерактивных консольных приложений. Можно быстро набросать CLI для внутреннего инструмента, которым будут пользоваться только ваши коллеги-разработчики, а можно создать полномасштабную REPL-среду программирования или интерфейс администрирования с десятками команд, контекстной справкой и продвинутой визуализацией данных.

Почему стоит создавать CLI в эпоху графических интерфейсов: преимущества и целевая аудитория



"Зачем вообще возиться с консолью, когда есть красивые графические интерфейсы?" — этот вопрос задают многие начинающие разработчики. И действительно, в мире блестящих React-приложений и пышных анимаций на Vue.js консольные инструменты кажутся динозаврами из другой эпохи. Но тут есть один нюанс: CLI никуда не исчезли именно потому, что они невероятно эффективны. Задумайтесь на секунду: Git, Docker, npm, pip — все эти инструменты, без которых современная разработка немыслима, работают в первую очередь через командную строку. И это не случайность или инерция мышления. CLI предлагают фундаментальные преимущества, которые графические интерфейсы не способны заменить.

Первое и самое очевидное — скорость. Одна короткая строка может заменить десяток кликов мышкой по разным частям интерфейса. На своём опыте могу сказать, что правильно настроенный терминал с автодополнением в разы ускоряет рутинные задачи.

Второе — автоматизация. CLI-инструменты легко встраиваются в скрипты, пайплайны и автоматизированые процесы. Невозможно представить современный CI/CD без консольных команд, запускающих тесты, сборку и деплой. Попробуйте автоматизировать 50 кликов в графическом интерфейсе — и вы поймёте, почему CLI до сих пор в цене.

Третий аспект, о котором часто забывают — надёжность. Графические интерфейсы меняются, элементы перемещаются, но текстовые команды стабильны. Команда rm -rf, которую я выучил 15 лет назад, работает точно также и сегодня. Никаких неожиданных обновлений дизайна, никаких "Sorry, we've moved that button".

Ещё одно преимущество — ресурсоэффективность. Графические приложения пожирают память и процессор, а CLI можно запустить даже на микроконтроллере. На старом VPS с 512 МБ RAM, где веб-интерфейс будет мучительно тормозить, консольное приложение полетит как птица.

Python GUI: создаём простое приложение с PyQt и Qt Designer
Хотел поинтересоваться по поводу этой статьи https://tproger.ru/translations/python-gui-pyqt/...

Невозможность преобразования cli::array<System::Int32, 1>^ в cli::array<int, 1>^
class Config: static String^ resultsFileName = &quot;results.rs&quot;; static array&lt;int^&gt;^...

Приложение "Стрельба по мишеням" на C++/CLI. Ошибка
Подскажите, пожалуйста, как исправить данные ошибки, и что-то мне подсказывает, что они имеют одну...

Работа нейросети из книги Тарика Рашида "Создаем нейронную сеть"
Здравствуйте! Вопрос наверное к тем, кто читал сабжевую книгу и понимает, что там за сетка...


Введение в Prompt Toolkit



Prompt Toolkit — это библиотека, которая превращает стандартный терминал в интерактивную среду с богатыми возможностями пользовательского взаимодействия. Она абстрагирует сложности работы с низкоуровневым контролем терминала, предоставляя разработчикам высокоуровневый API для создания впечатляющих консольных интерфейсов.

Python
1
2
3
4
from prompt_toolkit import prompt
 
user_input = prompt('Введите команду: ')
print(f'Вы ввели: {user_input}')
Даже этот простейший пример демонстрирует мощь библиотеки — вы получаете редактирование истории, перемещение по строке и другие возможности "из коробки". Но реальный потенциал Prompt Toolkit раскрывается, когда вы начинаете использовать его продвинутые функции.

Чем же Prompt Toolkit выделяется на фоне альтернатив? До его появления разработчики Python обычно использовали библиотеку readline (точнее, её Python-обертки) или создавали собственные решения. Существуют и другие библиотеки вроде cmd, click, argparse — но они решают совсем другие задачи, фокусируясь на обработке аргументов командной строки, а не на создании интерактивных интерфейсов. Ряд других библиотек, таких как python-inquirer или PyInquirer, также предоставляют возможности для создания интерактивных CLI, но они ограничены преопределёнными типами интерфейсов и не обладают гибкостью и глубиной Prompt Toolkit. Если эти библиотеки — набор готовых мебельных гарнитуров, то Prompt Toolkit — целая столярная мастерская с полным набором инструментов.

Ключевые возможности, которые делают Prompt Toolkit незаменимым в арсенале разработчика:

1. Автодополнение — настраиваемое и умное, с возможостью отображения множественых предложений, фильтрации, подсказок и всего, что только может прийти в голову.
2. Подсветка синтаксиса — позволяет выделять части введенного текста разными цветами, что особенно полезно для командных оболочек или языковых интерпретаторов.
3. Кастомные привязки клавиш — возможность назначать любые действия на комбинации клавиш, создавая интуитивные сочетания для частых операций.
4. Многострочное редактирование — в отличии от простых решений, Prompt Toolkit легко справляется с многострочным вводом, что критично для сложных команд или текстовых редакторов.
5. Поддержка мыши — да, вы не ослышались, даже в консольном приложении можно использовать мышь для выделения текста, кликов по элементам интерфейса и т.д.
6. Асинхронность — полноценная поддержка asyncio, позволяющая создавать отзывчивые интерфейсы, которые не блокируют выполнение программы.
7. Система расположения элементов — продвинутый механизм для создания сложных многокомпонентных интерфейсов с панелями, меню, полями ввода и другими элементами.

Я надолго застрял на своём первом проекте с Prompt Toolkit, потоу что постоянно открывал новые грани и возможности. Начинал с простого REPL для своего мини-языка, а закончил полноценной средой разработки с цветовыми схемами, автодополнением контекстно-зависимых конструкций, инлайн-документацией и древовидным обозревателем объектов. И всё это работало быстрее, чем средняя Electron-обёртка с блестящим интерфейсом.

Один из самых известных проектов, использующих Prompt Toolkit — это ptpython, альтернативная REPL-среда для Python с массой улучшений. Другой пример — ipython, который в новых версиях переключился на эту библиотеку для улучшения пользовательского опыта. Существуют также CLI для Azure, AWS и других сервисов, построенные на Prompt Toolkit. Но хватит теории — давайте посмотрим на простой пример, добавляющий автодополнение и подсветку:

Python
1
2
3
4
5
6
7
8
from prompt_toolkit import prompt
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.lexers import PygmentsLexer
from pygments.lexers.python import PythonLexer
 
python_completer = WordCompleter(['print', 'def', 'class', 'import', 'if', 'else', 'while', 'for', 'try', 'except'])
 
code = prompt('>>> ', lexer=PygmentsLexer(PythonLexer), completer=python_completer)
Вот так, всего несколькими строками кода, мы получили мини-REPL с подсветкой синтаксиса Python и автодополнением основных ключевых слов. И это только верхушка айсберга того, что можно сделать с Prompt Toolkit.

Экосистема Prompt Toolkit: взаимосвязь с другими библиотеками Python



Первое, что приходит на ум — интеграция с Pygments. Pygments обеспечивает подсветку синтаксиса для более чем 500 языков программирования и форматов, а Prompt Toolkit с радостью использует эту возможность через PygmentsLexer. Результат такого союза — ввод кода с профессиональной подсветкой синтаксиса прямо в консоли.

Python
1
2
3
4
from pygments.lexers.sql import SqlLexer
from prompt_toolkit.lexers import PygmentsLexer
 
sql_input = prompt('SQL> ', lexer=PygmentsLexer(SqlLexer))
Три строчки кода — и у вас есть консольный интерфейс с подсветкой SQL-синтаксиса. Магия? Нет, просто хорошо продуманная экосистема.

Следующий важный игрок в этой команде — docopt. Если вы хотите, чтобы ваше CLI-приложение принимало аргументы командной строки в стиле POSIX, эта библиотека — ваш лучший друг. Комбинация prompt_toolkit для интерактивного режима и docopt для парсинга аргументов создаёт мощный тандем. Я начинал один свой проект, думая что обойдусь только Prompt Toolkit, но быстро понял, что docopt идеально дополняет его для полноценного CLI.

Rich — еще одна жемчужина в короне консольных Python-библиотек. Её специализация — форматированый вывод в терминале: таблицы, прогресс-бары, деревья, богато оформленый текст. Когда нужно красиво представить данные, собранные через интерактивный интерфейс Prompt Toolkit, Rich приходит на помощь.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from rich.console import Console
from rich.table import Table
from prompt_toolkit import prompt
 
# Интерактивный ввод через Prompt Toolkit
query = prompt('Введите запрос: ')
 
# Красивый вывод результатов через Rich
console = Console()
table = Table(show_header=True)
table.add_column("ID")
table.add_column("Имя")
table.add_column("Значение")
 
# Представим, что это результат запроса
table.add_row("1", "Alpha", "100")
table.add_row("2", "Beta", "200")
 
console.print(table)
Получился интерактивный интерфейс с красивым табличным выводом. Каждая библиотека делает то, что умеет лучше всего.

Для создания полноценных приложений часто требуются асинхронные возможности, и здесь выходит связка Prompt Toolkit + asyncio. Встроеная поддержка асинхронности позволяет создавать интерфейсы, реагирующие на события в реальном времени.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
from prompt_toolkit import PromptSession
 
async def my_async_cli():
    session = PromptSession()
    while True:
        try:
            result = await session.prompt_async('> ')
            print(f'Вы ввели: {result}')
        except KeyboardInterrupt:
            break
 
asyncio.run(my_async_cli())
Для работы с файловой системой Prompt Toolkit отлично сочетается с pathlib — современной заменой старых функций из os.path. Реализовать автодополнение файловых путей с учётом всех нюансов операционной системы становится тривиальной задачей. Наверняка вы впечатлитесь, когда в первый раз увидите автодополнение имён файлов в своём CLI, работающее так же гладко, как в обычном системном терминале. А если вашему CLI понадобится работать с API — сочетание с httpx или aiohttp для асинхронных HTTP-запросов создаст отзывчивый интерфейс, который не блокируется при ожидании ответа от сервера. Пользователь может продолжать взаимодействие, пока запросы выполняются в фоне.

Даже библиотеки машинного обучения находят применение в связке с Prompt Toolkit. Представьте интерактивный анализатор данных с подсказками от модели scikit-learn, который предлагает оптимальные параметры на основе ввода пользователя. Или REPL для PyTorch с интелектуальным автодополнением тензорных операций.

Экосистема Prompt Toolkit не ограничивается сторонними библиотеками — она создала собственную мини-вселенную инструментов и расширений. Проекты вроде ptpython, pymux и pyvim демонстрируют, как далеко можно зайти, используя эту библиотеку как фундамент. Именно это взаимодействие с другими библиотеками делает Prompt Toolkit настолько мощным инструментом. Вам не нужно изобретать велосипед — просто соберите идеальный транспорт из уже существующих высококачественных деталей.

История создания и философия Prompt Toolkit: интервью с разработчиком



За каждой значимой библиотекой стоит история и человек с уникальным видением. Prompt Toolkit не исключение. Джонатан Слендерс, создатель этой мощной библиотеки, поделился интересными деталями о том, как родился проект, перевернувший представление многих о консольных интерфейсах в Python.
"Всё началось с разочарования," — с улыбкой признаётся Джонатан. "Я работал над проектом, требующим создания интерактивного командного интерфейса, и сущесвующие инструменты меня катострофически не устраивали. readline был мощным, но сложным и ограниченным, особенно в кроссплатформенном контексте. А высокоуровневые решения были слишком упрощёнными."
Первая версия Prompt Toolkit появилась в 2014 году и была своего рода экспериментом. Джонатан хотел создать библиотеку, которая бы сочетала гибкость низкоуровневых инструментов с простотой высокоуровневого API.

"Философия Prompt Toolkit выстроилась вокруг нескольких ключевых принципов," — рассказывает разработчик. "Во-первых, пользовательский интерфейс должен быть отзывчивым и никогда не блокировать выполнение программы. Во-вторых, API должен быть интуитивным — простые задачи должны требовать простого кода. И в-третьих, кроссплатформенность без компромиссов — то, что работает в Linux, должно так же хорошо работать в Windows и macOS."

Интересно, что изначально проект задумывался как внутренний инструмент для собственных нужд автора. Однако когда другие разработчики начали обращать внимание на его возможности, Джонатан осознал перспективы своего детища.
"Я помню момент озарения, когда понял, что можно создать не просто ещё одну библиотеку для ввода, а целый фреймворк для построения консольных интерфейсов. Это было захватывающе и немного пугающе одновременно."
От версии к версии Prompt Toolkit эволюционировал: появилась поддержка асинхронного программирования, система компоновки для сложных интерфейсов, улучшенная работа с мышью. Но что самое важное — сохранялась исходная философия.

"Когда разрабатываешь библиотеку, постоянно приходится балансировать между простотой и мощностью," — делится Джонатан. "Хотелось, чтобы новички могли легко начать, а продвинутые пользователи имели доступ ко всем возможностям. Думаю, это самый сложный аспект дизайна API."
Одним из поворотных моментов в истории библиотеки стало её принятие проектом IPython. Когда такие известные инструменты начали переходить на Prompt Toolkit, сообщество разработчиков обратило на библиотеку серьёзное внимание.
"IPython стал отличной проверкой на прочность," — смеётся Джонатан. "Когда твою библиотеку использует проект такого масштаба, быстро находятся все узкие места и потенциальные проблемы."

Что касается советов начинающим пользователям, создатель библиотеки рекомендует начать с простых примеров, а затем постепенно добавлять новые возможности:
"Не пытайтесь сразу реализовать все продвинутые функции. Начните с базового прототипа, заставьте его работать и только потом расширяйте. Prompt Toolkit спроектирован так, чтобы поддерживать итеративный подход."
Размышляя о будущем, Джонатан видит Prompt Toolkit не просто как инструмент для создания CLI, но как мост между консольным и графическим миром.
"Границы между CLI и GUI стираются. Современные терминалы поддерживают функции, о которых раньше можно было только мечтать. И Prompt Toolkit будет эволюционировать вместе с ними, предоставляя разработчикам всё больше возможностей для создания лучших пользовательских интерфейсов, независимо от того, в консоли они работают или нет."

Основы создания CLI с Prompt Toolkit



Начнем с базовой структуры приложения. Простейшее CLI на Prompt Toolkit можно создать буквально в три строки кода:

Python
1
2
3
4
from prompt_toolkit import prompt
 
user_input = prompt('> ')
print(f'Вы ввели: {user_input}')
Но такой минимализм редко удовлетворяет реальные потребности. Для серьёзного приложения нам понадобится более структурированный подход. Вот скелет типичного CLI-приложения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
 
def main():
    # Создаём сессию - она сохраняет историю и настройки между вызовами
    session = PromptSession()
    
    # Определяем команды нашего приложения
    commands = ['help', 'status', 'load', 'save', 'exit']
    completer = WordCompleter(commands)
    
    # Основной цикл приложения
    while True:
        # Получаем ввод с автодополнением
        user_input = session.prompt('> ', completer=completer)
        
        # Обрабатываем ввод
        if user_input == 'exit':
            break
        elif user_input == 'help':
            print("Доступные команды: " + ", ".join(commands))
        else:
            print(f"Выполняется команда: {user_input}")
 
if __name__ == '__main__':
    main()
Обратите внимание на PromptSession вместо простого prompt. Сессия позволяет сохранять историю команд между вызовами, что критично для удобного пользовательского опыта. Когда я создавал свой первый инструмент с Prompt Toolkit, именно отсутствие сессии стало главной причиной ругани пользователей — никому не нравится перенабирать одни и те же команды.

А что насчёт управления вводом пользователя? Большинство CLI-приложений основано на концепции REPL (Read-Evaluate-Print Loop): чтение ввода, его обработка, вывод результата и возврат к чтению. Этот паттерн настолько фундаментален, что стал стандартом де-факто для интерактивных интерфейсов. Вот как выглядит улучшенная версия с более гибкой обработкой команд:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
 
def handle_help(args):
    print("Доступные команды: help, status, load <filename>, save <filename>, exit")
 
def handle_load(args):
    if not args:
        print("Ошибка: необходимо указать имя файла")
        return
    print(f"Загружаем файл: {args[0]}")
 
def handle_save(args):
    if not args:
        print("Ошибка: необходимо указать имя файла")
        return
    print(f"Сохраняем в файл: {args[0]}")
 
def handle_status(args):
    print("Статус системы: всё работает отлично!")
 
def main():
    session = PromptSession()
    
    # Словарь команд и соответствующих обработчиков
    command_handlers = {
        'help': handle_help,
        'load': handle_load,
        'save': handle_save,
        'status': handle_status,
    }
    
    completer = WordCompleter(list(command_handlers.keys()) + ['exit'])
    
    while True:
        user_input = session.prompt('> ', completer=completer)
        
        # Разбиваем ввод на команду и аргументы
        parts = user_input.strip().split()
        if not parts:
            continue
            
        command, args = parts[0], parts[1:]
        
        if command == 'exit':
            break
        elif command in command_handlers:
            command_handlers[command](args)
        else:
            print(f"Неизвестная команда: {command}")
 
if __name__ == '__main__':
    main()
Такая структура с обработчиками команд упрощает расширение функциональности — просто добавьте новую функцию и запись в словарь command_handlers. Я обнаружил, что этот паттерн масштабируется очень хорошо даже для CLI с десятками команд.

Теперь перейдём к самой вкусной части — автодополнению. В предыдущем примере мы использовали простейший WordCompleter, который предлагает фиксированный список команд. Но возможности Prompt Toolkit гораздо шире. Для более сложных сценариев можно создать собственный Completer, реализовав всю нужную логику:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import Completer, Completion
 
class CustomCompleter(Completer):
    def __init__(self, commands):
        self.commands = commands
        
    def get_completions(self, document, complete_event):
        # Получаем текст до курсора
        text_before_cursor = document.text_before_cursor
        
        # Разбиваем на части, чтобы понять контекст
        parts = text_before_cursor.split()
        
        # Если ничего не введено или пробел в конце - предлагаем все команды
        if not parts or text_before_cursor.endswith(' '):
            for cmd in self.commands:
                yield Completion(cmd, start_position=0)
        # Иначе ищем совпадения с тем, что уже введено
        else:
            last_word = parts[-1]
            for cmd in self.commands:
                if cmd.startswith(last_word):
                    # Вычисляем, сколько символов нужно заменить
                    replace_len = len(last_word)
                    yield Completion(cmd, start_position=-replace_len)
 
def main():
    commands = ['help', 'status', 'load', 'save', 'exit']
    session = PromptSession(completer=CustomCompleter(commands))
    
    while True:
        user_input = session.prompt('> ')
        if user_input.strip() == 'exit':
            break
        print(f"Выполняется: {user_input}")
 
if __name__ == '__main__':
    main()
Но Prompt Toolkit предлагает и другие готовые реализации комплитеров для разных задач:
PathCompleter: автодополнение для путей к файлам и директориям,
NestedCompleter: продвинутое автодополнение с учётом контекста команды,
FuzzyCompleter: комплитер с нечётким совпадением, прощающий опечатки,
NestedCompleter особенно интересен для структурированных команд:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import NestedCompleter
 
def main():
    # Структура команд с вложенными подкомандами
    completer = NestedCompleter.from_nested_dict({
        'show': {
            'status': None,
            'log': None,
            'users': None,
        },
        'set': {
            'name': None,
            'age': None,
        },
        'exit': None,
    })
    
    session = PromptSession(completer=completer)
    
    while True:
        user_input = session.prompt('> ')
        if user_input.strip() == 'exit':
            break
        print(f"Выполняется: {user_input}")
 
if __name__ == '__main__':
    main()
С этим комплитером вы получите контекстные подсказки: после ввода "show " будут предложены только "status", "log" и "users".
Не могу не упомянуть про подсветку синтаксиса — это то, что превращает обычный ввод в профессиональный интерфейс. Вот простой пример подсветки команд:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from prompt_toolkit import PromptSession
from prompt_toolkit.lexers import Lexer
from prompt_toolkit.styles.named_colors import NAMED_COLORS
from prompt_toolkit.styles import Style
 
class CommandLexer(Lexer):
    def lex_document(self, document):
        def get_line(lineno):
            line = document.lines[lineno]
            parts = line.split()
            
            if not parts:
                return []
            
            # Выделяем команду зелёным, аргументы - синим
            command, args = parts[0], ' '.join(parts[1:])
            tokens = [
                (0, len(command), 'command'),
            ]
            
            if args:
                tokens.append((len(command) + 1, len(line), 'argument'))
            
            return tokens
        
        return get_line
 
def main():
    # Определяем стили для разных типов токенов
    style = Style.from_dict({
        'command': '#00AA00',  # Зелёный для команд
        'argument': '#0000AA', # Синий для аргументов
    })
    
    session = PromptSession(lexer=CommandLexer(), style=style)
    
    while True:
        user_input = session.prompt('> ')
        if user_input.strip() == 'exit':
            break
        print(f"Выполняется: {user_input}")
 
if __name__ == '__main__':
    main()
Для продвинутых приложений часто требуется не только получать ввод, но и валидировать его. Prompt Toolkit предлагает элегантное решение с помощью валидаторов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from prompt_toolkit import PromptSession
from prompt_toolkit.validation import Validator, ValidationError
 
class NumberValidator(Validator):
    def validate(self, document):
        text = document.text
        if not text.isdigit():
            raise ValidationError(message='Пожалуйста, введите число')
 
session = PromptSession(validator=NumberValidator())
number = session.prompt('Введите число: ')
print(f'Вы ввели число: {number}')
Такой подход позволяет мгновенно сообщать пользователю об ошибках, не дожидаясь отправки команды.
Ещё одна важная составляющая хорошего CLI — история команд. По умолчанию Prompt Toolkit запоминает введённые команды только на время сессии, но можно настроить сохранение между запусками:

Python
1
2
3
4
from prompt_toolkit import PromptSession
from prompt_toolkit.history import FileHistory
 
session = PromptSession(history=FileHistory('.myapp-history'))
Теперь история будет автоматически сохраняться в файл .myapp-history. Пользователи оценят возможность восстановить ранее введённые команды с помощью клавиш со стрелками — такие мелочи делают интерфейс по-настоящему удобным.

Обработка ошибок пользовательского ввода и создание интуитивных сообщений



Давайте признаемся честно: любой интерфейс взаимодействия с пользователем — это всегда минное поле. Люди не читают инструкций, печатают с ошибками, пытаются ввести недопустимые значения или просто экспериментируют, нажимая случайные клавиши. В мире GUI все привыкли к красивым диалоговым окнам и информативным подсказкам. И если ваше CLI-приложение просто падает с трейсбеком Python при каждой ошибке пользователя — оно отправится в мусорную корзину быстрее, чем вы успеете сказать "exception handling". Prompt Toolkit предлагает элегантные механизмы для обработки пользовательского ввода, которые помогут избежать большинства потенцальных проблем. Я в своё время потратил несколько дней, разбираясь с этой частью библиотеки, но теперь каждый раз благодарю себя за это инвестированое время.

Главный инструмент в нашем арсенале — валидаторы. Они проверяют ввод пользователя и сигнализируют об ошибках в реальном времени, еще до отправки команды. Это потрясающе удобно, поскольку пользователь получает мгновенную обратную связь.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from prompt_toolkit import PromptSession
from prompt_toolkit.validation import Validator, ValidationError
 
class CommandValidator(Validator):
    def validate(self, document):
        text = document.text
        
        # Базовая проверка на пустую строку
        if not text.strip():
            raise ValidationError(message='Команда не может быть пустой')
            
        # Проверка на допустимые команды
        valid_commands = ['help', 'status', 'load', 'save', 'exit']
        command = text.split()[0]
        
        if command not in valid_commands:
            raise ValidationError(
                message=f'Неизвестная команда: {command}. Доступные команды: {", ".join(valid_commands)}',
                cursor_position=len(command)  # Подсветка только команды, не всего ввода
            )
 
session = PromptSession(validator=CommandValidator())
Заметьте параметр cursor_position — эта маленькая деталь размещает сообщение об ошибке именно там, где она произошла, а не в конце строки. Такие мелочи делают интерфейс значительно более дружелюбным.
Для более сложных проверок можно комбинировать валидаторы или создавать многоступенчатую логику:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LoadCommandValidator(Validator):
    def validate(self, document):
        text = document.text.strip()
        parts = text.split()
        
        if not parts:
            return  # Пустой ввод пока допустим, пользователь ещё печатает
            
        if parts[0] != 'load':
            return  # Это не команда load, пусть другие валидаторы разбираются
            
        if len(parts) < 2:
            raise ValidationError(
                message='Укажите имя файла: load <filename>',
                cursor_position=len(text)
            )
            
        filename = parts[1]
        import os
        if not os.path.exists(filename):
            raise ValidationError(
                message=f'Файл {filename} не существует',
                cursor_position=len(parts[0]) + 1 + len(filename)
            )
Отдельный класс ошибок — это невалидные аргументы. Например, когда пользователь вводит текст там, где ожидается число, или отрицательное значение там, где требуется положительное. Для таких случаев стоит создать специализированные валидаторы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class IntegerValidator(Validator):
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        
    def validate(self, document):
        text = document.text
        
        if not text.strip():
            return  # Пустая строка - пользователь ещё печатает
            
        try:
            value = int(text)
        except ValueError:
            raise ValidationError(message='Введите целое число')
            
        if self.min_value is not None and value < self.min_value:
            raise ValidationError(message=f'Значение должно быть не меньше {self.min_value}')
            
        if self.max_value is not None and value > self.max_value:
            raise ValidationError(message=f'Значение должно быть не больше {self.max_value}')
 
# Использование
age = session.prompt('Введите возраст: ', validator=IntegerValidator(min_value=0, max_value=120))
Но валидация — лишь часть головоломки. Иногда ошибки невозможно предотвратить заранее. Например, при попытке сохранить файл может закончиться место на диске или произойти проблема с правами доступа. В таких случаях стоит использовать информативные сообщения об ошибках:

Python
1
2
3
4
5
6
7
try:
    with open(filename, 'w') as f:
        f.write(data)
except PermissionError:
    print('\033[91mОшибка: недостаточно прав для записи в файл. Попробуйте выбрать другой путь.\033[0m')
except IOError as e:
    print(f'\033[91mНе удалось сохранить данные: {str(e)}\033[0m')
Заметьте использование ANSI-кодов для выделения сообщения красным цветом. Prompt Toolkit предлагает более элегантный способ через свою систему стилей, но иногда простой подход эффективнее.

Отдельное внимание стоит уделить интерактивным подсказкам. Когда пользователь совершает ошибку, вместо сухого сообщения можно предложить варианты исправления:

Python
1
2
3
4
5
6
7
8
9
10
def handle_unknown_command(command):
    import difflib
    valid_commands = ['help', 'status', 'load', 'save', 'exit']
    # Ищем похожие команды
    matches = difflib.get_close_matches(command, valid_commands)
    
    if matches:
        print(f"Команда '{command}' не найдена. Возможно, вы имели в виду: {', '.join(matches)}?")
    else:
        print(f"Неизвестная команда: {command}. Введите 'help' для списка доступных команд.")

Оптимизация производительности ввода в больших приложениях



Когда ваше приложение на Prompt Toolkit из скромного скрипта превращается в монстра с сотнями команд, тысячами вариантов автодополнения и глубокой вложенностью контекстов — проблемы производительности начинают заявлять о себе самым неприятным образом. Тормозящий интерфейс и задержки между нажатием клавиши и появлением символа — верный способ довести пользователя до белого каления.

Первое, с чем обычно сталкиваются разработчики крупных CLI — это проседание скорости автодополнения. Когда комплитер должен перебирать тысячи вариантов, даже современные компьютеры начинают задыхаться. Я столкнулся с этим, когда мой инструмент для анализа логов пытался предлагать автодополнение из списка в 50 000 уникальных идентификаторов сессий. Решение? Ленивые генераторы и инкрементальный поиск:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LazyCompleter(Completer):
    def __init__(self, get_completions_func):
        self.get_completions_func = get_completions_func
        
    def get_completions(self, document, complete_event):
        word_before_cursor = document.get_word_before_cursor()
        
        # Не начинаем поиск, пока пользователь не набрал хотя бы 2 символа
        if len(word_before_cursor) < 2:
            return
            
        # Получаем только первые 100 совпадений для мгновенной реакции
        for completion in islice(self.get_completions_func(word_before_cursor), 100):
            yield completion
Второй подход — кэширование результатов. Часто пользователи работают с одним и тем же набором данных или повторяют схожие паттерны ввода. Почему бы не использовать это?

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import lru_cache
 
class CachedCompleter(Completer):
    def __init__(self, base_completer):
        self.base_completer = base_completer
        
    def get_completions(self, document, complete_event):
        return self.get_cached_completions(document.text_before_cursor, document.cursor_position)
            
    @lru_cache(maxsize=1000)
    def get_cached_completions(self, text, position):
        # Создаём новый документ, так как оригинальный нельзя хэшировать для lru_cache
        from prompt_toolkit.document import Document
        doc = Document(text, position)
        return list(self.base_completer.get_completions(doc, None))
Следующая болевая точка — валидация ввода. Сложные валидаторы, выполняющие множество проверок, могут существенно замедлить работу интерфейса, особенно если они запускаются при каждом нажатии клавиши.
Для оптимизации нужно отложить тяжелые проверки до момента, когда пользователь закончил ввод:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LazyValidator(Validator):
    def __init__(self, immediate_validator=None, full_validator=None):
        # Лёгкий валидатор для мгновенной обратной связи
        self.immediate_validator = immediate_validator
        # Тяжелый валидатор для финальной проверки
        self.full_validator = full_validator
        
    def validate(self, document):
        # Сначала запускаем лёгкую проверку
        if self.immediate_validator:
            self.immediate_validator.validate(document)
            
        # Тяжелую проверку запускаем только если пользователь закончил ввод
        # (например, по наличию определенного символа в конце)
        if document.text.endswith(';') and self.full_validator:
            self.full_validator.validate(document)
Даже история команд может стать узким местом, если содержит тысячи записей. При каждом нажатии стрелки "вверх" или "вниз" Prompt Toolkit перебирает элементы истории, что при больших объемах может вызывать заметные задержки.
Решение — ограничить размер истории в памяти и использовать эффективное хранение:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
from prompt_toolkit.history import InMemoryHistory, FileHistory
 
# Ограничиваем историю последними 1000 командами
class LimitedHistory(FileHistory):
    def __init__(self, filename, max_entries=1000):
        super().__init__(filename)
        self.max_entries = max_entries
        self._loaded_strings = self._loaded_strings[-max_entries:] if len(self._loaded_strings) > max_entries else self._loaded_strings
        
    def append_string(self, string):
        super().append_string(string)
        if len(self._loaded_strings) > self.max_entries:
            self._loaded_strings = self._loaded_strings[-self.max_entries:]
Наконец, для действительно крупных приложений асинхронный подход становится не опцией, а необходимостью. Используйте asyncio для всех операций, которые потенциально могут блокировать интерфейс:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async def main():
    session = PromptSession()
    
    # Запускаем фоновые задачи параллельно с вводом
    background_task = asyncio.create_task(background_processing())
    
    try:
        while True:
            command = await session.prompt_async('> ')
            # Обработка команды...
    finally:
        # Корректно завершаем фоновые задачи
        background_task.cancel()
        try:
            await background_task
        except asyncio.CancelledError:
            pass
Оптимизация CLI-приложений — это искусство балансирования между отзывчивостью интерфейса и функциональностью. Тщательное профилирование и поиск узких мест позволят вам создать инструмент, который остаётся быстрым даже при масштабировании до промышленных размеров.

Персонализация ввода пользователя: создание кастомных валидаторов для различных типов данных



Валидация ввода — краеугольный камень любого качественного интерфейса, а в мире командной строки эта задача приобретает особую важность. Когда пользователь работает с графическим интерфейсом, у него есть выпадающие списки, ползунки, календари и прочие виджеты, ограничивающие ввод. В CLI всё иначе — перед пользователем чистый холст текстового поля, где можно написать что угодно, включая откровенную чушь, которая отправит ваше приложение в царство исключений.

Создание кастомных валидаторов позволяет обуздать эту стихию и направить пользователя в нужное русло. Prompt Toolkit предоставляет для этого гибкий механизм, который я активно использую в своих проектах уже несколько лет. Базовая структура валидатора проста — это класс, наследующийся от Validator и реализующий метод validate(). Внутри метода мы анализируем текст документа и при обнаружении ошибок выбрасываем ValidationError с понятным сообщением:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from prompt_toolkit.validation import Validator, ValidationError
 
class EmailValidator(Validator):
    def validate(self, document):
        text = document.text
        
        if not text:  # Пустой ввод разрешен на этапе набора
            return
            
        if '@' not in text:
            raise ValidationError(message='Email должен содержать символ @')
            
        if not text.split('@')[1].strip():
            raise ValidationError(message='Укажите домен после @')
            
        # Можно добавить более сложные проверки для доменов, спецсимволов и т.д.
Этот простой валидатор проверяет наличие символа @ в введенной строке и домена после него. Конечно, в реальном приложении проверка email-адреса потребует более сложной логики, включая регулярные выражения.
Давайте рассмотрим несколько специализированных валидаторов для различных типов данных:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class DateValidator(Validator):
    def validate(self, document):
        text = document.text
        
        if not text:
            return
            
        try:
            from datetime import datetime
            # Пробуем распарсить дату в формате ДД.ММ.ГГГГ
            datetime.strptime(text, '%d.%m.%Y')
        except ValueError:
            raise ValidationError(message='Неверный формат даты. Используйте формат ДД.ММ.ГГГГ')
 
 
class URLValidator(Validator):
    def validate(self, document):
        text = document.text
        
        if not text:
            return
            
        if not (text.startswith('http://') or text.startswith('https://')):
            raise ValidationError(message='URL должен начинаться с http:// или https://')
            
        # Дополнительные проверки...
А вот более сложный пример — валидатор для проверки SQL-запросов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SQLQueryValidator(Validator):
    def __init__(self, allowed_tables=None):
        self.allowed_tables = allowed_tables or []
        
    def validate(self, document):
        text = document.text.lower()
        
        if not text:
            return
            
        # Запрещаем опасные операции
        dangerous_keywords = ['drop', 'truncate', 'delete without where', 'update without where']
        for keyword in dangerous_keywords:
            if keyword in text:
                raise ValidationError(message=f'Потенциально опасная операция: {keyword}')
        
        # Проверяем, обращается ли запрос только к разрешенным таблицам
        if self.allowed_tables:
            import re
            tables_in_query = re.findall(r'from\s+(\w+)', text)
            for table in tables_in_query:
                if table not in self.allowed_tables:
                    raise ValidationError(message=f'Таблица {table} не доступна для запросов')
Особенно интересная возможность — комбинирование валидаторов. Допустим, у нас есть поле, принимающее либо число, либо специальное значение "auto". Создадим для него составной валидатор:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CompositeValidator(Validator):
    def __init__(self, validators):
        self.validators = validators
        
    def validate(self, document):
        # Документ валиден, если прошел хотя бы через один валидатор
        text = document.text
        errors = []
        
        for validator in self.validators:
            try:
                validator.validate(document)
                return  # Валидация прошла успешно
            except ValidationError as e:
                errors.append(str(e))
        
        # Если ни один валидатор не сработал, выбрасываем ошибку
        raise ValidationError(message='Неверный формат ввода: ' + '; '.join(errors))
 
# Использование
number_or_auto = CompositeValidator([
    IntegerValidator(),
    Validator(lambda text: None if text == 'auto' else ValidationError('Ожидается число или "auto"'))
])
Такой подход позволяет создавать сколь угодно сложные правила проверки данных. В одном из своих проектов я реализовал валидатор для мини-языка запросов, который проверял синтаксис, типы данных и даже выполнял семантический анализ выражений. Пользователи были в восторге от того, как интрфейс подсвечивал ошибки прямо в процессе набора, совсем как в современных IDE. А вот валидатор с зависимостью от контекста — проверяет, что ввод соответствует формату, предписаному командой:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ContextAwareValidator(Validator):
    def validate(self, document):
        text = document.text
        parts = text.split()
        
        if not parts:
            return
            
        command = parts[0]
        
        if command == 'set-timer' and len(parts) > 1:
            # Для команды set-timer ожидаем время в формате ЧЧ:ММ:СС
            try:
                import re
                if not re.match(r'^\d{2}:\d{2}:\d{2}$', parts[1]):
                    raise ValidationError(
                        message='Неверный формат времени. Используйте ЧЧ:ММ:СС',
                        cursor_position=len(command) + 1
                    )
            except Exception:
                raise ValidationError(
                    message='Неверный формат времени',
                    cursor_position=len(command) + 1
                )

Системы подсказок и документации внутри CLI-приложения



Даже самый продуманный командный интерфейс рискует остаться бесполезным, если пользователь не понимает, как им пользоваться. Разработчики часто забывают, что встроеная документация — не просто формальность, а важнейший компонент CLI-приложения, способный превратить ваш инструмент из набора непонятных команд в мощное и доступное средство решения задач.

В мире консольных утилит сложилось несколько традиций документирования: классические man-страницы в Unix, флаг --help, который вывдит краткую справку, и интерактивные системы помощи вроде тех, что есть в bash или PowerShell. Prompt Toolkit позволяет реализовать любой из этих подходов, но также открывает двери к гораздо более интересным решениям. Начнём с самого простого — встроенной команды "help":

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def handle_help(args):
    if not args:
        print("\nДоступные команды:\n")
        print("  help [command]   - Показать справку о команде")
        print("  load <filename>  - Загрузить данные из файла")
        print("  save <filename>  - Сохранить данные в файл")
        print("  status           - Показать текущий статус")
        print("  exit             - Выйти из программы\n")
    else:
        command = args[0]
        if command == "load":
            print("\nload <filename> - Загружает данные из указанного файла.")
            print("Поддерживаемые форматы: .json, .csv, .xml\n")
        elif command == "save":
            print("\nsave <filename> - Сохраняет текущие данные в указанный файл.")
            print("Если расширение не указано, используется .json\n")
        else:
            print(f"Справка по команде '{command}' отсутствует.")
Но это примитивный подход, который быстро превращается в беспорядок при росте числа команд. Гораздо элегантнее использовать структурированную систему документации:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# Словарь с документацией по командам
COMMAND_DOCS = {
    'load': {
        'syntax': 'load <filename>',
        'description': 'Загружает данные из указанного файла.',
        'examples': ['load data.json', 'load ~/documents/config.xml'],
        'args': [{'name': 'filename', 'description': 'Путь к файлу для загрузки'}]
    },
    'save': {
        'syntax': 'save <filename>',
        'description': 'Сохраняет текущие данные в указанный файл.',
        'examples': ['save output.json', 'save ./backup/data.csv'],
        'args': [{'name': 'filename', 'description': 'Путь для сохранения файла'}]
    }
    # И так далее для других команд...
}
 
def show_documentation(command=None):
    if command is None:
        # Показываем список всех команд
        print("\nДоступные команды:\n")
        for cmd, data in COMMAND_DOCS.items():
            print(f"  {data['syntax']:<20} - {data['description']}")
        return
    
    if command not in COMMAND_DOCS:
        print(f"Справка по команде '{command}' отсутствует.")
        return
    
    doc = COMMAND_DOCS[command]
    print(f"\n{doc['syntax']}\n")
    print(f"Описание: {doc['description']}\n")
    
    if doc.get('args'):
        print("Аргументы:")
        for arg in doc['args']:
            print(f"  {arg['name']:<15} - {arg['description']}")
        print()
    
    if doc.get('examples'):
        print("Примеры:")
        for example in doc['examples']:
            print(f"  {example}")
        print()
Однако настоящая мощь Prompt Toolkit раскрывается в контекстных подсказках, которые появляются прямо во время ввода. Помню, как пользователи моего инструмента для парсинга логов были в восторге от подсказок, возникающих под курсором, когда они останавливались на пару секунд при вводе сложной команды.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import Completer, Completion
 
class DocumentedCompleter(Completer):
    def __init__(self, commands_docs):
        self.commands_docs = commands_docs
        
    def get_completions(self, document, complete_event):
        text = document.text_before_cursor
        word = document.get_word_before_cursor()
        
        for command, doc in self.commands_docs.items():
            if command.startswith(word):
                display_meta = doc['description']
                yield Completion(
                    command, 
                    start_position=-len(word),
                    display_meta=display_meta
                )
 
# Использование
session = PromptSession(
    completer=DocumentedCompleter(COMMAND_DOCS),
    complete_while_typing=True
)
А для вывода подробной документации прямо в момент ввода можно использовать нижнюю панель приложения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.dimension import Dimension
 
class DocumentationBar:
    def __init__(self, command_docs):
        self.command_docs = command_docs
        self.current_text = ""
        
    def get_formatted_text(self):
        if not self.current_text:
            return [("class:doc", "Введите команду или 'help' для списка команд")]
        
        parts = self.current_text.strip().split()
        if not parts:
            return [("class:doc", "Введите команду или 'help' для списка команд")]
            
        command = parts[0]
        if command not in self.command_docs:
            return [("class:doc", f"Неизвестная команда: {command}")]
            
        doc = self.command_docs[command]
        return [("class:doc.syntax", f"{doc['syntax']} - {doc['description']}")]
    
    def create_window(self):
        return Window(
            FormattedTextControl(self.get_formatted_text),
            height=Dimension(min=1, max=1)
        )
Такая система документации делает ваше CLI-приложение дружелюбным даже для новичков и экономит время опытным пользователям, избавляя их от необходимости запоминать все детали синтаксиса.

Продвинутые техники



Начнём с создания полноценных интерактивных меню. В примитивных CLI мы часто видим нумерованные списки: "Нажмите 1 для X, 2 для Y..." Но это прошлый век! С Prompt Toolkit можно построить элегантное, навигируемое с клавиатуры меню, которое выглядит и ощущается как в графическом интерфейсе:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
from prompt_toolkit.shortcuts import radiolist_dialog
 
result = radiolist_dialog(
    title="Выберите режим работы",
    text="Доступные режимы:",
    values=[
        ("debug", "Отладка - подробный вывод всех операций"),
        ("normal", "Обычный режим - только важные сообщения"),
        ("silent", "Тихий режим - никаких уведомлений")
    ]
).run()
 
print(f"Выбран режим: {result}")
Это диалоговое окно с радиокнопками, по которому можно перемещаться стрелками! В терминале! Я до сих пор вспоминаю лица коллег, когда впервые продемонстрировал им такой интерфейс в своём инструменте для тестирования сетевых сервисов.
Но это только начало. Prompt Toolkit предлагает целый набор диалогов "из коробки":

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from prompt_toolkit.shortcuts import checkboxlist_dialog, message_dialog, yes_no_dialog, input_dialog
 
# Диалог с множественным выбором
options = checkboxlist_dialog(
    title="Настройка параметров",
    text="Выберите требуемые опции:",
    values=[
        ("logging", "Включить логирование"),
        ("autocommit", "Автоматическое сохранение"),
        ("backup", "Создавать резервные копии")
    ]
).run()
 
# Информационное сообщение
message_dialog(
    title="Операция завершена",
    text="Все данные успешно обработаны!"
).run()
 
# Запрос подтверждения
if yes_no_dialog(
    title="Внимание!",
    text="Вы уверены, что хотите удалить все файлы?"
).run():
    # Выполняем удаление
    pass
 
# Ввод текста в диалоге
name = input_dialog(
    title="Регистрация",
    text="Введите ваше имя:"
).run()
Следующий уровень мастерства — многострочное редактирование. В отличии от стандартного input(), Prompt Toolkit может работать с многострочным текстом, превращая ваше CLI в полноценный текстовый редактор:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from prompt_toolkit import Application
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
from prompt_toolkit.layout.layout import Layout
 
# Создаём буфер для хранения и редактирования текста
buffer = Buffer()
buffer.text = "def hello_world():\n    print('Hello, world!')\n"
 
# Формируем пользовательский интерфейс
root_container = HSplit([
    # Заголовок
    Window(height=1, content=FormattedTextControl("Редактор кода - Ctrl+Q для выхода")),
    # Редактор
    Window(content=BufferControl(buffer=buffer)),
    # Статусная строка
    Window(height=1, content=FormattedTextControl("Готово к редактированию"))
])
 
# Создаём и запускаем приложение
layout = Layout(root_container)
app = Application(layout=layout, full_screen=True)
result = app.run()
 
# По завершении, можно использовать введёный текст
print(f"Вы ввели:\n{buffer.text}")
Вот это уже совсем другой уровень! Вместо однострочного ввода мы создали полноэкранный редактор с заголовком и статусной строкой. Можно наполнять buffer.text содержимым файла, реализовывать подсветку синтаксиса, нумерацию строк и другие функции современных редакторов кода.
Работа со стилями — ещё одна область, где Prompt Toolkit блистает. Многие разработчики ограничиваются базовыми ANSI-цветами, но с Prompt Toolkit вы получаете полноценную систему стилей, вдохновленную CSS:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from prompt_toolkit.styles import Style
 
# Определяем набор стилей
style = Style.from_dict({
    # Классы для основных элементов интерфейса
    'dialog':             'bg:#222222',
    'dialog.body':        'bg:#000000 #ffffff',
    'dialog.title':       'bg:#0000aa #ffffff',
    
    # Стили для кнопок
    'button':             'bg:#222222',
    'button.focused':     'bg:#aaaaaa #000000',
    
    # Стили для текстовых элементов
    'text-area':          'bg:#000000 #ffffff',
    'text-area.cursor':   'bg:#aaaaaa',
    
    # Кастомные классы для нашего приложения
    'status.bar':         'bg:#222222 #aaaaaa',
    'error.message':      'bg:#550000 #ffffff bold',
    'warning.message':    'bg:#553300 #ffffff',
    'success.message':    'bg:#005500 #ffffff',
})
 
# Применяем стиль к приложению
app = Application(style=style, ...)
Мощь этой системы стилей трудно переоценить. Я помню, как создавал утилиту для мониторинга производительности, где различные показатели подсвечивались в зависимости от их значения: нормальные зелёным, предупреждающие жёлтым, критические красным. Всё это занимало буквально несколько строк кода благодаря гибкой системе стилей.
Для ещё большего контроля над форматированием, можно использовать HTML-подобный синтаксис:

Python
1
2
3
4
5
6
7
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit import print_formatted_text
 
print_formatted_text(HTML(
    '<style bg="blue" fg="white">Информация: </style> '
    'Процесс завершён успешно. Обработано <i>145</i> файлов за <b>3.2</b> секунды.'
))
Такой подход позволяет создавать богатые, информативные вывовы, где важные данные выделяются визуально, что значительно повышает удобство использования CLI.
Особенно интересная возможность — динамическое изменение стилей в зависимости от состояния приложения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class DynamicStyle:
    def __init__(self):
        self.is_error_mode = False
        
    def get_style(self):
        # Базовые стили, общие для всех режимов
        styles = {
            'prompt': 'bold',
            'bottom-toolbar': 'bg:#222222 #aaaaaa',
        }
        
        # Добавляем специфичные стили в зависимости от режима
        if self.is_error_mode:
            styles.update({
                'prompt': 'bg:#550000 #ffffff bold',
                'bottom-toolbar': 'bg:#550000 #ffffff',
            })
        
        return Style.from_dict(styles)
 
# Использование
dynamic_style = DynamicStyle()
session = PromptSession(style=lambda: dynamic_style.get_style())
 
# При возникновении ошибки
dynamic_style.is_error_mode = True
Сочетая все эти техники, можно создавать интерфейсы, которые адаптируются к контексту, выделяют важную информацию и обеспечивают интуитивное взаимодействие с пользователем.
Один из самых мощных, но недооценённых инструментов — это система компоновки Prompt Toolkit. Она позволяет разделить экран на несколько зон и организовать сложные, многокомпонентные интерфейсы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from prompt_toolkit.layout.containers import VSplit, HSplit, Window
from prompt_toolkit.layout.dimension import Dimension
 
# Создаём сложный интерфейс, разделённый на зоны
layout = Layout(
    HSplit([
        # Верхняя панель - заголовок
        Window(height=1, content=FormattedTextControl('Мониторинг системы')),
        
        # Основная область - разделена на две колонки
        VSplit([
            # Левая панель - список ресурсов
            Window(width=Dimension(preferred=30, max=40), content=FormattedTextControl('Список ресурсов...')),
            
            # Правая панель - детальная информация
            Window(content=FormattedTextControl('Детальная информация...'))
        ]),
        
        # Нижняя панель - статусная строка
        Window(height=1, content=FormattedTextControl('Готово | Память: 45% | CPU: 12%'))
    ])
)
Такой подход к созданию интерфейса открывает безграничные возможности: информационные панели, мониторы системных ресурсов, файловые менеджеры — всё это становится реализуемым в терминале. Фактически, вы получаете мини-фреймворк для разработки текстовых интерфейсов TUI (Text User Interface), который конкурирует со специализированными библиотеками вроде curses.
Одной из наиболее востребованных продвинутых функций Prompt Toolkit является настройка клавиатурных сочетаний. Благодаря гибкой системе привязки клавиш, можно создать интуитивные горячие клавиши для любых действий:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from prompt_toolkit.key_binding import KeyBindings
 
kb = KeyBindings()
 
@kb.add('c-q')  # Ctrl+Q
def exit_(event):
    """Выход из приложения."""
    event.app.exit()
 
@kb.add('c-s')  # Ctrl+S
def save(event):
    """Сохранение данных."""
    event.app.current_buffer.save_to_disk()
 
@kb.add('f5')  # F5
def refresh(event):
    """Обновление данных."""
    event.app.refresh_screen()
Особенно мощной эта система становится при создании модальных интерфейсов в стиле vim, где одни и те же клавиши выполняют разные действия в зависимости от текущего режима. Можно определить условные привязки:

Python
1
2
3
4
5
6
7
@kb.add('i', filter=vi_navigation_mode)
def enter_insertion_mode(event):
    event.app.vi_state.input_mode = InputMode.INSERT
 
@kb.add('escape', filter=vi_insertion_mode)
def enter_navigation_mode(event):
    event.app.vi_state.input_mode = InputMode.NAVIGATION

Интеграция с асинхронным кодом и событийно-ориентированное программирование в CLI



Prompt Toolkit спроектирован с учётом реалий асинхронного мира. Эта библиотека идеально сочетается с экосистемой asyncio, позволяя создавать отзывчивые интерфейсы, которые никогда не блокируют ввод пользователя. Когда я впервые столкнулся с этой стороной Prompt Toolkit, она буквально перевернула мое представление о возможностях CLI. Базовый пример асинхронного приложения выглядит так:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import asyncio
from prompt_toolkit import PromptSession
from prompt_toolkit.patch_stdout import patch_stdout
 
async def main():
    session = PromptSession()
    
    # Важно использовать patch_stdout при смешивании асинхронного ввода и обычного вывода
    with patch_stdout():
        while True:
            try:
                result = await session.prompt_async('> ')
                print(f'Вы ввели: {result}')
            except KeyboardInterrupt:
                return
            except EOFError:
                return
 
asyncio.run(main())
Обратите внимание на prompt_async вместо обычного prompt и использование patch_stdout. Эта комбинация позволяет корректно обрабатывать асинхронный ввод без конфликтов с выводом. Но настоящая магия начинается, когда вы комбинируете асинхронный ввод с фоновыми задачами:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async def background_task():
    while True:
        # Имитация длительной операции
        await asyncio.sleep(5)
        print('\nФоновая задача выполняет действие...')
 
async def main():
    session = PromptSession()
    
    # Запускаем фоновую задачу
    background_job = asyncio.create_task(background_task())
    
    try:
        with patch_stdout():
            while True:
                command = await session.prompt_async('> ')
                if command == 'exit':
                    break
                print(f'Выполняется: {command}')
    finally:
        # Корректно завершаем фоновую задачу
        background_job.cancel()
        try:
            await background_job
        except asyncio.CancelledError:
            pass
 
asyncio.run(main())
Этот пример демонстрирует ключевой принцип: пока пользователь думает над вводом команды, приложение может выполнять другие полезные действия — загружать данные, обновлять кэш, проверять статус системы. Всё это происходит без блокировки интерфейса.

Не менее важным аспектом является событийно-ориентированное программирование. В традиционном CLI мы просто реагируем на готовые команды, но Prompt Toolkit позволяет создавать интерфейсы, реагирующие на отдельные нажатия клавиш, движения мыши и другие низкоуровневые события:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from prompt_toolkit.application import Application
from prompt_toolkit.layout import Layout
from prompt_toolkit.key_binding import KeyBindings
 
kb = KeyBindings()
 
@kb.add('c-c')
def _(event):
    """Выход по Ctrl+C."""
    event.app.exit()
 
@kb.add('c-space')
def _(event):
    """Реакция на Ctrl+Space."""
    print("Активирована специальная функция!")
 
app = Application(key_bindings=kb, full_screen=True)
app.run()
Эта модель дает потрясающую гибкость. В одном из своих проектов я создал интерфейс, который анализировал вводимый текст в реальном времени, на каждое нажатие клавиши, и запускал интеллектуальные подсказки ещё до того, как пользователь заканчивал ввод. Пользователи были в восторге от такой отзывчивости.
Продвинутый пример — ассинхронный интерфейс с обработкой пользовательских событий:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import asyncio
from prompt_toolkit.application import Application
from prompt_toolkit.layout import Layout, Window
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.widgets import TextArea
from prompt_toolkit.layout.containers import HSplit
from prompt_toolkit.formatted_text import HTML
 
kb = KeyBindings()
status_bar_text = HTML("<ansired>Ожидание...</ansired>")
 
async def slow_operation():
    global status_bar_text
    status_bar_text = HTML("<ansigreen>Выполняется операция...</ansigreen>")
    await asyncio.sleep(3)  # Имитация длительной операции
    status_bar_text = HTML("<ansiblue>Операция завершена!</ansiblue>")
    await asyncio.sleep(2)
    status_bar_text = HTML("<ansired>Ожидание...</ansired>")
 
@kb.add('f5')
async def _(event):
    """Запуск асинхронной операции по F5."""
    # Создаём и запускаем задачу, но не ждём её завершения
    asyncio.create_task(slow_operation())
 
@kb.add('c-q')
def _(event):
    """Выход по Ctrl+Q."""
    event.app.exit()
 
def get_status_text():
    return status_bar_text
 
text_area = TextArea(text="Нажмите F5 для запуска операции или Ctrl+Q для выхода")
status_bar = Window(height=1, content=get_status_text)
 
app = Application(
    layout=Layout(HSplit([text_area, status_bar])),
    key_bindings=kb,
    full_screen=True
)
 
app.run()
Здесь мы создаём приложение, которое реагирует на F5, запуская длительную операцию, но при этом интерфейс остаётся полностью отзывчивым — пользователь может продолжать вводить текст, пока операция выполняется в фоне.

Создание анимаций и визуальных эффектов в консольных приложениях



Основа всех консольных анимаций — контроль над выводом и тайминги. Начнём с простейшего примера — анимированого индикатора загрузки:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
import time
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import HTML
 
def spinner_animation():
    frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
    for _ in range(10):  # Крутимся 10 циклов
        for frame in frames:
            print_formatted_text(HTML(f"<ansiyellow>{frame}</ansiyellow> Загрузка данных..."), end='\r')
            time.sleep(0.1)
    print()  # Новая строка после окончания
 
spinner_animation()
Хитрость в использовании символа \r (возврат каретки), который перемещает курсор в начало текущей строки без перевода строки. Это позволяет перезаписывать содержимое "на месте", создавая иллюзию движения.
Но мы можем пойти значительно дальше. Вот пример полноценного прогресс-бара с процентами и изменяющимся цветом:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time
import asyncio
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.patch_stdout import patch_stdout
 
async def progress_bar(total, description="Прогресс"):
    with patch_stdout():
        for i in range(total + 1):
            percentage = i / total
            bar_length = 40
            filled_length = int(bar_length * percentage)
            
            # Изменяем цвет в зависимости от прогресса
            if percentage < 0.3:
                color = "red"
            elif percentage < 0.6:
                color = "yellow"
            else:
                color = "green"
                
            bar = '█' * filled_length + '░' * (bar_length - filled_length)
            
            print_formatted_text(
                HTML(f"<{color}>{description}: [{bar}] {percentage:.1%}</{color}>"),
                end='\r'
            )
            
            await asyncio.sleep(0.05)  # Имитация работы
        
        print()  # Новая строка после завершения
 
asyncio.run(progress_bar(100, "Скачивание файлов"))
Обратите внимание на связку с asyncio — эта анимация не блокирует выполнение других операций, что критично для отзывчивого приложения.
Еще один крутой эффект — "печатающийся" текст, словно кто-то набирает его вживую:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import HTML
from prompt_toolkit.patch_stdout import patch_stdout
 
async def typewriter_effect(text, delay=0.07):
    with patch_stdout():
        for char in text:
            print_formatted_text(char, end='', flush=True)
            await asyncio.sleep(delay)
        print()  # Новая строка в конце
 
asyncio.run(typewriter_effect("Добро пожаловать в будущее консольных интерфейсов!"))
Такие эффекты особенно хороши для приветственных сообщений или важных уведомлений — они привлекают внимание пользователя и создают вау-эффект.
Для настоящих ценителей UI-дизайна могу предложить изменение цвета фона в реальном времени. Техника странная, но эффектная:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import asyncio
import colorsys
from prompt_toolkit import print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.patch_stdout import patch_stdout
 
async def rainbow_background(text, cycles=3):
    with patch_stdout():
        width = 60  # Ширина текстового поля
        
        # Создаём пустой блок нужной ширины
        blank = ' ' * width
        
        for cycle in range(cycles * 100):
            # Преобразуем HSV в RGB для плавного перехода цвета
            h = cycle / 100.0
            r, g, b = colorsys.hsv_to_rgb(h, 0.8, 0.8)
            
            # Конвертируем в диапазон 0-255
            r, g, b = int(r*255), int(g*255), int(b*255)
            
            # Создаём строку с фоновым цветом
            style = f'bg:#{r:02x}{g:02x}{b:02x}'
            
            # Центрируем текст
            padded_text = text.center(width)
            
            # Выводим строки с цветным фоном
            tokens = FormattedText([
                (style, blank),
                (style, padded_text),
                (style, blank)
            ])
            
            print_formatted_text(tokens, end='\r')
            await asyncio.sleep(0.02)
        
        print("\n" * 3)  # Очистка после завершения
 
asyncio.run(rainbow_background("✨ Prompt Toolkit Rocks! ✨"))
Используя подобные эффекты с умом, можно создать по-настоящему впечатляющий интерфейс, который не будет уступать графическим аналогам по визуальной привлекательности.

Локализация CLI-приложений: поддержка многоязычности и региональных настроек



Мой первый опыт создания мультиязычного CLI чуть не закончился катастрофой, когда я просто загнал все строки в Google Translate. Коллеги из Германии потом неделю присылали скриншоты с особо "удачными" переводами, а японские партнёры вообще не поняли половины интерфейса. С тех пор я подхожу к локализации гораздо серьёзнее.
Базовая интернационализация с Prompt Toolkit начинается с выноса всех текстовых строк в отдельные словари:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# translations.py
messages = {
    'en': {
        'welcome': 'Welcome to our CLI!',
        'prompt': 'Enter command: ',
        'goodbye': 'Thank you for using our tool!',
        'not_found': 'Command not found: {}',
    },
    'ru': {
        'welcome': 'Добро пожаловать в наш CLI!',
        'prompt': 'Введите команду: ',
        'goodbye': 'Спасибо за использование нашего инструмента!',
        'not_found': 'Команда не найдена: {}',
    },
    'de': {
        'welcome': 'Willkommen in unserem CLI!',
        'prompt': 'Befehl eingeben: ',
        'goodbye': 'Vielen Dank für die Nutzung unseres Tools!',
        'not_found': 'Befehl nicht gefunden: {}',
    }
}
Затем создаём простую функцию-обёртку для работы с переводами:

Python
1
2
3
4
5
6
7
8
def get_message(key, language='en', *args, **kwargs):
    if language not in messages:
        language = 'en'  # Фолбэк на английский
    
    message = messages[language].get(key, messages['en'].get(key, key))
    if args or kwargs:
        return message.format(*args, **kwargs)
    return message
Теперь интерфейс можно адаптировать под язык пользователя:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import locale
from prompt_toolkit import prompt
from translations import get_message
 
# Определяем язык системы или используем переданный параметр
def get_system_language():
    try:
        # Пытаемся получить настройки локали
        system_locale = locale.getlocale()[0]
        if system_locale:
            return system_locale.split('_')[0]
    except:
        pass
    return 'en'
 
user_language = get_system_language()
print(get_message('welcome', user_language))
 
while True:
    command = prompt(get_message('prompt', user_language))
    if command == 'exit':
        print(get_message('goodbye', user_language))
        break
    # Обработка команды...
Но настоящий вызов — это различия в форматировании дат, чисел и других культурно-зависимых элементов. Немцы используют запятую в качестве десятичного разделителя, американцы пишут месяц перед днём, а в Японии совсем другой календарь! Библиотека babel отлично решает эти проблемы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from babel.dates import format_date, format_time
from babel.numbers import format_number, format_currency
import datetime
 
# Форматирование даты согласно локали
now = datetime.datetime.now()
print(format_date(now, locale='en_US'))  # Sept 15, 2023
print(format_date(now, locale='de_DE'))  # 15.09.2023
print(format_date(now, locale='ja_JP'))  # 2023/09/15
 
# Форматирование чисел
amount = 1234.56
print(format_number(amount, locale='en_US'))  # 1,234.56
print(format_number(amount, locale='de_DE'))  # 1.234,56
print(format_number(amount, locale='ru_RU'))  # 1 234,56
 
# Валюта
print(format_currency(amount, 'USD', locale='en_US'))  # $1,234.56
print(format_currency(amount, 'EUR', locale='de_DE'))  # 1.234,56 €
Отдельное внимание стоит уделить правильному отображению символов в консоли. Убедитесь, что ваше приложение использует UTF-8 и корректно обрабатывает символы разных алфавитов:

Python
1
2
3
4
5
6
7
8
import sys
import codecs
 
# Настройка кодировки для ввода/вывода
if sys.stdout.encoding != 'utf-8':
    sys.stdout = codecs.getwriter('utf-8')(sys.stdout.buffer, 'strict')
if sys.stderr.encoding != 'utf-8':
    sys.stderr = codecs.getwriter('utf-8')(sys.stderr.buffer, 'strict')
Еще один нюанс — направление текста. Для языков с письмом справа налево (арабский, иврит) необходимы специальные адаптации. Prompt Toolkit частично поддерживает такие языки, но может потребоваться дополнительная настройка терминала пользователя. В конце концов, хорошая локализация CLI — это не просто технический трюк, а проявление уважения к пользователям со всего мира. Поверьте моему опыту: правильно локализованное приложение вызывает у интернациональной аудитории те же эмоции, что и любимая песня на родном языке.

Полный пример приложения



Теперь, когда мы освоили основы и продвинутые техники Prompt Toolkit, самое время объединить всё изученное в одном полноценном приложении. Давайте создадим многофункциональный менеджер задач, который будет демонстрировать большинство рассмотренных возможностей библиотеки. Этот пример не просто покажет, как складывать элементы вместе — он даст вам шаблон для собственных интерактивных консольных приложений. Наш менеджер задач будет обладать следующими возможностями:
  1. Интерактивное меню с автодополнением команд.
  2. Добавление, просмотр, редактирование и удаление задач.
  3. Организация задач по приоритету и категориям.
  4. Поиск задач с нечетким совпадением.
  5. Интерактивные диалоги для подтверждения действий.
  6. Асинхронные операции для сохранения и загрузки данных.
  7. Анимированные индикаторы прогресса.
  8. Многоязычность интерфейса.
  9. Персонализируемые цветовые схемы.

Давайте начнём с проектирования структуры приложения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# task_manager.py
import asyncio
import json
import os
from datetime import datetime
from typing import Dict, List, Optional, Union, Any
 
from prompt_toolkit import PromptSession, print_formatted_text, HTML
from prompt_toolkit.completion import NestedCompleter
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit.shortcuts import radiolist_dialog, input_dialog, yes_no_dialog
from prompt_toolkit.styles import Style
from prompt_toolkit.patch_stdout import patch_stdout
 
# Определение модели данных
class Task:
    def __init__(self, title: str, priority: str = 'medium', 
                 category: str = 'general', due_date: Optional[str] = None,
                 completed: bool = False):
        self.id = None  # Будет установлен при добавлении в TaskManager
        self.title = title
        self.priority = priority
        self.category = category
        self.due_date = due_date
        self.completed = completed
        self.created_at = datetime.now().isoformat()
        
    def to_dict(self) -> Dict[str, Any]:
        return {
            'id': self.id,
            'title': self.title,
            'priority': self.priority,
            'category': self.category,
            'due_date': self.due_date,
            'completed': self.completed,
            'created_at': self.created_at
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'Task':
        task = cls(
            title=data['title'],
            priority=data.get('priority', 'medium'),
            category=data.get('category', 'general'),
            due_date=data.get('due_date'),
            completed=data.get('completed', False)
        )
        task.id = data.get('id')
        task.created_at = data.get('created_at', datetime.now().isoformat())
        return task
Здесь мы определили базовый класс Task, который представляет задачу с различными атрибутами. Теперь создадим класс TaskManager для управления задачами:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class TaskManager:
    def __init__(self, data_file: str = 'tasks.json'):
        self.tasks: List[Task] = []
        self.data_file = data_file
        self.next_id = 1
        
    def add_task(self, task: Task) -> Task:
        task.id = self.next_id
        self.tasks.append(task)
        self.next_id += 1
        return task
    
    def get_task(self, task_id: int) -> Optional[Task]:
        for task in self.tasks:
            if task.id == task_id:
                return task
        return None
    
    def update_task(self, task_id: int, **kwargs) -> Optional[Task]:
        task = self.get_task(task_id)
        if not task:
            return None
        
        for key, value in kwargs.items():
            if hasattr(task, key):
                setattr(task, key, value)
        
        return task
    
    def delete_task(self, task_id: int) -> bool:
        task = self.get_task(task_id)
        if not task:
            return False
        
        self.tasks.remove(task)
        return True
    
    def get_all_tasks(self) -> List[Task]:
        return self.tasks
    
    def get_tasks_by_category(self, category: str) -> List[Task]:
        return [task for task in self.tasks if task.category == category]
    
    def get_tasks_by_priority(self, priority: str) -> List[Task]:
        return [task for task in self.tasks if task.priority == priority]
    
    def search_tasks(self, query: str) -> List[Task]:
        query = query.lower()
        return [task for task in self.tasks 
                if query in task.title.lower() or 
                   query in task.category.lower()]
    
    async def save_to_file(self) -> None:
        """Асинхронно сохраняет задачи в файл"""
        with open(self.data_file, 'w') as f:
            json.dump({
                'next_id': self.next_id,
                'tasks': [task.to_dict() for task in self.tasks]
            }, f, indent=2)
    
    async def load_from_file(self) -> bool:
        """Асинхронно загружает задачи из файла"""
        if not os.path.exists(self.data_file):
            return False
        
        try:
            with open(self.data_file, 'r') as f:
                data = json.load(f)
                self.next_id = data.get('next_id', 1)
                self.tasks = [Task.from_dict(t) for t in data.get('tasks', [])]
            return True
        except (json.JSONDecodeError, KeyError):
            return False
Класс TaskManager обеспечивает базовые операции CRUD (создание, чтение, обновление, удаление) для задач, а также поддерживает поиск и фильтрацию. Методы save_to_file и load_from_file асинхронные, что позволит нам сохранять данные без блокировки интерфейса.
Теперь создадим класс TaskApp, который будет отвечать за пользовательский интерфейс:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
class TaskApp:
    def __init__(self, manager: TaskManager, language: str = 'en'):
        self.manager = manager
        self.language = language
        self.session = PromptSession()
        self.running = False
        self.kb = KeyBindings()
        
        # Определение стилей для приложения
        self.style = Style.from_dict({
            'title': 'ansiyellow bold',
            'priority.high': 'ansired bold',
            'priority.medium': 'ansiyellow',
            'priority.low': 'ansigreen',
            'category': 'ansiblue',
            'task.completed': 'ansibrightblack',
            'status.bar': 'bg:ansibrightblack ansiwhite',
            'dialog': 'bg:ansiblack ansiwhite',
            'dialog.title': 'bg:ansiblue ansiwhite bold',
            'spinner': 'ansiyellow'
        })
        
        # Настройка комплитера команд
        self.completer = NestedCompleter.from_nested_dict({
            'add': None,
            'list': {
                'all': None,
                'category': None,
                'priority': {
                    'high': None,
                    'medium': None,
                    'low': None
                }
            },
            'edit': None,
            'delete': None,
            'search': None,
            'help': None,
            'exit': None
        })
        
        # Настройка горячих клавиш
        @self.kb.add('c-c')
        def _(event):
            """Выход по Ctrl+C."""
            self.running = False
            event.app.exit()
    
    # Переводы для интерфейса
    def get_message(self, key, *args, **kwargs):
        messages = {
            'en': {
                'welcome': 'Welcome to Task Manager! Type "help" for commands.',
                'prompt': 'Task Manager> ',
                'help_title': 'Available Commands:',
                'help_add': 'add - Add a new task',
                'help_list': 'list - List tasks (all/category/priority)',
                'help_edit': 'edit <id> - Edit a task',
                'help_delete': 'delete <id> - Delete a task',
                'help_search': 'search <query> - Search for tasks',
                'help_exit': 'exit - Exit the application',
                # ... другие переводы
            },
            'ru': {
                'welcome': 'Добро пожаловать в Менеджер Задач! Введите "help" для справки.',
                'prompt': 'Менеджер Задач> ',
                'help_title': 'Доступные команды:',
                'help_add': 'add - Добавить новую задачу',
                'help_list': 'list - Список задач (all/category/priority)',
                'help_edit': 'edit <id> - Редактировать задачу',
                'help_delete': 'delete <id> - Удалить задачу',
                'help_search': 'search <query> - Поиск задач',
                'help_exit': 'exit - Выйти из приложения',
                # ... другие переводы
            }
            # Можно добавить другие языки
        }
        
        if self.language not in messages:
            self.language = 'en'
            
        msg = messages[self.language].get(key, messages['en'].get(key, key))
        if args or kwargs:
            return msg.format(*args, **kwargs)
        return msg
    
    async def show_spinner(self, message, task):
        """Показывает анимированный спиннер во время выполнения задачи"""
        spinner_chars = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
        i = 0
        
        with patch_stdout():
            while not task.done():
                char = spinner_chars[i]
                i = (i + 1) % len(spinner_chars)
                print_formatted_text(
                    FormattedText([
                        ('class:spinner', char + ' '),
                        ('', message)
                    ]),
                    end='\r'
                )
                await asyncio.sleep(0.1)
            
            # Очищаем строку со спиннером
            print(' ' * (len(message) + 2), end='\r')
Теперь реализуем основные методы для интерфейса:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
    async def handle_command(self, command: str) -> None:
        """Обрабатывает введенные пользователем команды"""
        parts = command.strip().split(maxsplit=1)
        cmd = parts[0].lower() if parts else ""
        args = parts[1] if len(parts) > 1 else ""
        
        if cmd == 'add':
            await self.add_task_dialog()
        elif cmd == 'list':
            await self.list_tasks(args)
        elif cmd == 'edit':
            try:
                task_id = int(args)
                await self.edit_task_dialog(task_id)
            except ValueError:
                print_formatted_text(HTML("<ansired>Invalid task ID!</ansired>"))
        elif cmd == 'delete':
            try:
                task_id = int(args)
                await self.delete_task_dialog(task_id)
            except ValueError:
                print_formatted_text(HTML("<ansired>Invalid task ID!</ansired>"))
        elif cmd == 'search':
            if not args:
                print_formatted_text(HTML("<ansired>Please provide a search query</ansired>"))
            else:
                await self.search_tasks(args)
        elif cmd == 'help':
            self.show_help()
        elif cmd == 'exit':
            self.running = False
        else:
            print_formatted_text(HTML(f"<ansired>Unknown command: {cmd}</ansired>"))
            
    async def add_task_dialog(self) -> None:
        """Диалог добавления новой задачи"""
        title = await asyncio.to_thread(
            lambda: input_dialog(
                title='Add Task',
                text='Enter task title:'
            ).run()
        )
        
        if not title:
            return
            
        priority = await asyncio.to_thread(
            lambda: radiolist_dialog(
                title='Task Priority',
                text='Select priority:',
                values=[
                    ('high', 'High'),
                    ('medium', 'Medium'),
                    ('low', 'Low')
                ],
                default='medium'
            ).run()
        )
        
        if not priority:
            priority = 'medium'
            
        category = await asyncio.to_thread(
            lambda: input_dialog(
                title='Task Category',
                text='Enter category:'
            ).run()
        )
        
        if not category:
            category = 'general'
            
        # Создаем и добавляем задачу
        task = Task(title=title, priority=priority, category=category)
        self.manager.add_task(task)
        
        # Асинхронно сохраняем в файл с анимацией
        save_task = asyncio.create_task(self.manager.save_to_file())
        await self.show_spinner("Saving task...", save_task)
        
        print_formatted_text(HTML(f"<ansigreen>Task added successfully!</ansigreen>"))
Реализация остальных методов для взаимодействия с задачами оставлена для самостоятельного добавления. Наконец, добавим основную функцию для запуска приложения:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    async def run(self) -> None:
        """Запускает основной цикл приложения"""
        # Загружаем задачи из файла с анимацией
        load_task = asyncio.create_task(self.manager.load_from_file())
        await self.show_spinner("Loading tasks...", load_task)
        
        print_formatted_text(HTML(f"<ansiyellow>{self.get_message('welcome')}</ansiyellow>"))
        
        self.running = True
        with patch_stdout():
            while self.running:
                try:
                    command = await self.session.prompt_async(
                        self.get_message('prompt'),
                        completer=self.completer,
                        key_bindings=self.kb,
                        style=self.style
                    )
                    if command.strip():
                        await self.handle_command(command)
                except KeyboardInterrupt:
                    self.running = False
                except EOFError:
                    self.running = False
        
        # Сохраняем задачи перед выходом
        save_task = asyncio.create_task(self.manager.save_to_file())
        await self.show_spinner("Saving tasks before exit...", save_task)
        print_formatted_text(HTML("<ansigreen>Goodbye!</ansigreen>"))
 
# Запуск приложения
async def main():
    manager = TaskManager('tasks.json')
    app = TaskApp(manager)
    await app.run()
 
if __name__ == "__main__":
    asyncio.run(main())
Этот пример демонстрирует, как можно интегрировать различные функции Prompt Toolkit в единое приложение. Менеджер задач поддерживает все основные CRUD-операции, имеет интерактивный интерфейс с автодополнением команд, анимированные индикаторы для длительных операций и систему локализации.

Обратите внимание на несколько ключевых моментов:
1. Асинхронность: мы используем asyncio для неблокирующих операций с файлами и отображения анимаций, что делает интерфейс отзывчивым.
2. Стили: приложение использует кастомные стили для различных элементов интерфейса, что улучшает читаемость и визуальное восприятие.
3. Интерактивные диалоги: вместо простого запроса ввода, приложение использует диалоговые окна, что даёт более структурированный процесс взаимодействия.
4. Мультиязычность: система локализации позволяет легко переключаться между языками интерфейса.
5. Анимация: спиннеры визуально информируют пользователя о выполнении фоновых задач.

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

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async def list_tasks(self, args: str = '') -> None:
    """Отображает список задач с возможностью фильтрации"""
    parts = args.strip().split()
    filter_type = parts[0] if parts else 'all'
    
    tasks = []
    if filter_type == 'all':
        tasks = self.manager.get_all_tasks()
    elif filter_type == 'category' and len(parts) > 1:
        category = parts[1]
        tasks = self.manager.get_tasks_by_category(category)
    elif filter_type == 'priority' and len(parts) > 1:
        priority = parts[1]
        tasks = self.manager.get_tasks_by_priority(priority)
    else:
        tasks = self.manager.get_all_tasks()
    
    self._display_tasks(tasks)
 
async def search_tasks(self, query: str) -> None:
    """Ищет задачи по запросу"""
    tasks = self.manager.search_tasks(query)
    print_formatted_text(HTML(f"<ansiyellow>Search results for '{query}':</ansiyellow>"))
    self._display_tasks(tasks)
 
def _display_tasks(self, tasks: List[Task]) -> None:
    """Внутренний метод для форматированного отображения списка задач"""
    if not tasks:
        print_formatted_text(HTML("<ansibrightblack>No tasks found</ansibrightblack>"))
        return
        
    for task in tasks:
        title_style = 'task.completed' if task.completed else f'priority.{task.priority}'
        title_text = f"{task.title} {'✓' if task.completed else ''}"
        
        print_formatted_text(FormattedText([
            ('', f"[{task.id}] "),
            (f'class:{title_style}', title_text),
            ('', ' - '),
            ('class:category', task.category)
        ]))
Эти методы предоставляют основной функционал для отображения и поиска задач. Метод _display_tasks является внутренним вспомогательным методом, который форматирует задачи для вывода с использованием соответствующих стилей.

Такая архитектура позволяет легко расширять возможности приложения, добавляя новые команды или улучшая существующие. Например, можно добавить возможность экспорта задач в другие форматы, функции напоминаний о сроках выполнения или даже интеграцию с внешними сервисами.

Мы создаем игру, в которой главный герой маг. Он может убивать монстров заклинаниями, но для каждого монстра треб
Мы создаем игру, в которой главный герой маг. Он может убивать монстров заклинаниями, но для...

Словарь из имени пользователя и сумма за ним закрепленная, создаем новый пустой словарь , чтобы туда сохранить изменения
UserName = {'Vasya':500, 'Misha':500, 'Kolya':500, 'Petya':500, 'Oleg':500} new_Users ={} ...

Задача 1: Создаем игры
Создаем игры В последнее время достаточно популярной механикой в играх становится управление...

Установка CUDA Toolkit вручную
Всем здравствуйте! Я установил CUDA отсюда, установка прошла успешно. При проверке API вывод...

pyautogui.prompt
есть код: import os import pyautogui import pyperclip pyautogui.prompt(text='Искать в...

Использование оператора delete в C++/CLI
Желательно-ли использовать оператор delete в C++/CLI, для быстродействия, хотя под .NET есть...

Использование ссылок в CLI
Как использовать ссылки, например Pen^ pen = gcnew Pen(color); int w=4; pen-&gt;Width=&amp;w; чтобы...

Преобразование в верхний и нижний регистры в CLI
Здравствуйте, возникла проблема в консольном приложении CLI Код: #include &quot;stdafx.h&quot; ...

Книга по С++/CLI для Visual Studio 2008
скачал Visual Studio 2008 там есть раздел Visual С++ -&gt; CLR -&gt; приложение windows forms; ...

Основные отличия C# от C++ CLI
Основные отличия C# от C++/CLI: 1. Native код С++/CLI (управляемый C++) — это расширение C++,...

С++/CLI VS2008 создать глобальный управляемый динамический массив Point
Нужен глобальный динамический массив типа Point для переопределения события onPaint. При нажатии...

Пpoблeмa с отображением русского языка (Windows Forms C++ CLI)
Столкнулся с проблемой: я пишу программу, которая скачивает файл из интернета, а затем вынимает от...

Метки asyncio, cli, python
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Популярные LM модели ориентированы на увеличение затрат ресурсов пользователями сгенерированного кода (грязь -заслуги чистоплюев).
Hrethgir 12.06.2025
Вообще обратил внимание, что они генерируют код (впрочем так-же ориентированы разработчики чипов даже), чтобы пользователь их использующий уходил в тот или иной убыток. Это достаточно опытные модели,. . .
Топ10 библиотек C для квантовых вычислений
bytestream 12.06.2025
Квантовые вычисления - это та область, где теория встречается с практикой на границе наших знаний о физике. Пока большая часть шума вокруг квантовых компьютеров крутится вокруг языков высокого уровня. . .
Dispose и Finalize в C#
stackOverflow 12.06.2025
Работая с C# больше десяти лет, я снова и снова наблюдаю одну и ту же историю: разработчики наивно полагаются на сборщик мусора, как на волшебную палочку, которая решит все проблемы с памятью. Да,. . .
Повышаем производительность игры на Unity 6 с GPU Resident Drawer
GameUnited 11.06.2025
Недавно копался в новых фичах Unity 6 и наткнулся на GPU Resident Drawer - штуку, которая заставила меня присвистнуть от удивления. По сути, это внутренний механизм рендеринга, который автоматически. . .
Множества в Python
py-thonny 11.06.2025
В Python существует множество структур данных, но иногда я сталкиваюсь с задачами, где ни списки, ни словари не дают оптимального решения. Часто это происходит, когда мне нужно быстро проверять. . .
Работа с ccache/sccache в рамках C++
Loafer 11.06.2025
Утилиты ccache и sccache занимаются тем, что кешируют промежуточные результаты компиляции, таким образом ускоряя последующие компиляции проекта. Это означает, что если проект будет компилироваться. . .
Настройка MTProxy
Loafer 11.06.2025
Дополнительная информация к инструкции по настройке MTProxy: Перед сборкой проекта необходимо добавить флаг -fcommon в конец переменной CFLAGS в Makefile. Через crontab -e добавить задачу: 0 3. . .
Изучаем Docker: что это, как использовать и как это работает
Mr. Docker 10.06.2025
Суть Docker проста - это платформа для разработки, доставки и запуска приложений в контейнерах. Контейнер, если говорить образно, это запечатанная коробка, в которой находится ваше приложение вместе. . .
Тип Record в C#
stackOverflow 10.06.2025
Многие годы я разрабатывал приложения на C#, используя классы для всего подряд - и мне это казалось естественным. Но со временем, особенно в крупных проектах, я стал замечать, что простые классы. . .
Разработка плагина для Minecraft
Javaican 09.06.2025
За годы существования Minecraft сформировалась сложная экосистема серверов. Оригинальный (ванильный) сервер не поддерживает плагины, поэтому сообщество разработало множество альтернатив. CraftBukkit. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru