TTY
События терминального ввода, стилизованный вывод, surface представления и локальные виртуальные viewport.
process.lua на обычном process.host владеет виртуальным терминалом, если порождён с грантом viewport. Без такого присоединения модуль возвращает "no terminal context".
Загрузка
local tty = require("tty")
Модель
Surface — это исключительная аренда представления, которую один процесс держит на своём терминальном порту. Он публикует целые снимки строк; вычислением различий и восстановлением терминала владеет бэкенд. На порту одновременно может быть открыт только один surface.
Canvas — внутрипроцессный буфер композиции стилизованных ячеек. Он обрезает содержимое по границам ячеек и никогда не выдаёт собственных управляющих команд терминала.
Viewport — локальная структурированная граница терминала, позволяющая одному процессу размещать surface другого процесса без совместного использования байтовых потоков. Оболочка решает, где появится содержимое viewport, и транслирует ввод в координаты дочернего процесса; дочерний процесс видит обычный терминальный порт и не знает, полноэкранный он, размещён в мозаике, во вкладке или скрыт.
Viewport локальны для одной ноды среды исполнения. Гранты и хэндлы — это непрозрачные локальные возможности, а не сериализуемые сетевые ссылки.
Цикл ввода
Запустите доставку ввода, подпишитесь на события и обработайте их в цикле:
local tty = require("tty")
local io = require("io")
local function handler()
local events = tty.events()
tty.start()
while true do
local ev = events:receive()
if not ev then break end
if ev.type == "key" then
if ev.key == "q" or (ev.ctrl and ev.key == "c") then
break
end
io.print("Key: " .. ev.key)
elseif ev.type == "resize" then
io.print("Size: " .. ev.width .. "x" .. ev.height)
end
end
tty.stop()
end
Вызывайте events() до start(), чтобы потребитель был готов к моменту прихода первых событий. На виртуальном порту start() открывает доставку событий от зрителя к производителю, а stop() её закрывает: Viewport:send() вне этого интервала завершается ошибкой, а не молча теряет ввод. Доставка событий изменения размера не зависит от состояния ввода.
Управление вводом
tty.start()
Запустить доставку ввода для текущего порта. Физический терминал переключается в raw-режим.
local ok, err = tty.start()
Возвращает: boolean, error
tty.stop()
Остановить доставку ввода и вернуть терминал в нормальный режим.
local ok, err = tty.stop()
Возвращает: boolean, error
tty.events()
Подписаться на события терминала этого порта и вернуть канал. События доставляются в виде таблиц с полем type. Подпишитесь один раз и переиспользуйте канал.
local events, err = tty.events()
Возвращает: EventChannel, error
У EventChannel есть receive() и case_receive(), поэтому он сочетается с channel.select.
tty.screen_size()
Запросить текущие размеры терминала.
local width, height, err = tty.screen_size()
Возвращает: number, number, error
tty.mouse(enable)
Включить или отключить отслеживание событий мыши.
local ok, err = tty.mouse(true)
| Параметр | Тип | Описание |
|---|---|---|
enable |
boolean | true для включения, false для отключения |
Возвращает: boolean, error
Surface
Surface — это аренда представления на порту. Получите её, публикуйте целые кадры и закройте, когда закончите.
tty.surface(options?)
local surface, err = tty.surface({
alternate_screen = true,
hide_cursor = true,
synchronized_output = true,
})
| Опция | Тип | По умолчанию | Описание |
|---|---|---|---|
alternate_screen |
boolean | false | Выводить на альтернативный экранный буфер терминала |
hide_cursor |
boolean | false | Скрывать курсор терминала, пока surface открыт |
synchronized_output |
boolean | false | Оборачивать каждый кадр маркерами синхронизированного вывода |
Возвращает: Surface, error
Открытие второго surface на порту, где он уже есть, завершается ошибкой. Виртуальный порт хранит опции как метаданные surface; физический порт транслирует их в режимы терминала и восстанавливает при закрытии.
surface:present(rows, options?)
Публикует полный массив строк кадра. Строка 1 — верхняя.
local stats, err = surface:present(rows, {
cursor = {x = 12, y = 3, visible = true},
images = {
{placement_id = "logo", image = logo, x = 2, y = 2, cols = 20, rows = 8, alt = "Logo"},
},
})
| Параметр | Тип | Описание |
|---|---|---|
rows |
string[] | Полный кадр, не более 16384 строк |
options.cursor |
table | {x, y, visible} в координатах surface с отсчётом от единицы |
options.images |
table[] | Полный набор размещений сохраняемых изображений для кадра |
Опущенный cursor сохраняет последнее явно заданное состояние курсора. При наличии cursor все три его поля обязательны.
Возвращает: stats, error — неизменяемая запись с полями rows, changed_rows и bytes_written. Физический кадр, идентичный предыдущему, ничего не пишет.
surface:invalidate()
Забыть состояние представления бэкенда, не стирая логический кадр. Следующий present будет зафиксирован, даже если строки не изменились. Используйте после изменения размера внешнего терминала или когда физическое состояние мог нарушить другой владелец.
Возвращает: boolean
surface:close()
Освободить аренду. Идемпотентно: последующие вызовы возвращают результат первого закрытия. Физический бэкенд восстанавливает режимы терминала.
Возвращает: boolean, error
surface:capabilities()
Возвращает {images = "native" | "kitty" | "pending" | "none"}. Перед
проверкой запустите ввод терминала. Физический бэкенд может ненадолго вернуть
pending, пока запрашивает сведения о терминале; виртуальные surface сохраняют
изображения без проверки.
Возвращает: table, error
surface:clipboard(text)
Записывает запрос к буферу обмена OSC 52 на физическом surface. Текст должен быть корректным UTF-8 и занимать не более 65 536 байт. Успех означает, что вывод терминала принял запрос; политика терминала всё равно может его проигнорировать. Виртуальные surface возвращают ошибку неподдерживаемой операции; API не предоставляет чтение буфера обмена или подтверждение.
Возвращает: boolean, error
Сохраняемые изображения
Импортируйте PNG в ограниченное хранилище среды исполнения, затем поместите его хэндл в полный кадр surface:
local image = assert(tty.image(png_bytes))
local info = image:info() -- id, format, width, height, bytes
assert(surface:present(rows, {images = {{
placement_id = "preview",
image = image,
x = 1, y = 1, cols = 40, rows = 12,
src = {x = 0, y = 0, width = info.width, height = info.height},
z = 1,
alt = "Preview",
}}}))
tty.image() асинхронно проверяет байты PNG. image:read() явно экспортирует
закодированные байты, а image:close() освобождает ссылку. Координаты исходных
пикселей отсчитываются от нуля; координаты целевых ячеек — от единицы. Если в
последующем вызове present опустить images, прежние размещения будут
очищены. Физические терминалы, не поддерживающие изображения, показывают
текст alt размещения, а виртуальные surface сохраняют ресурс изображения
для зрителей.
Canvas
Canvas — ограниченный буфер стилизованных ячеек, используемый для композиции кадра перед выводом.
tty.canvas(width, height)
local canvas = tty.canvas(width, height)
Ширина ограничена 16384 колонками, высота — 16384 строками, а площадь — 262 144 ячейками. Аргументы вне диапазона вызывают ошибку аргумента.
Возвращает: Canvas
Рисование принимает стилизованный текст, а не команды терминала. Цвета SGR и ссылки OSC 8 сохраняются; стирание, перемещение курсора и другой чисто управляющий вывод не выдаётся. Каждое размещение обрезается независимо по границам ячеек с учётом ширины графем, поэтому обрезанная escape-последовательность не может просочиться в соседнее содержимое.
canvas:clear(fill?)
Очищает все ячейки. Необязательная стилизованная строка fill повторяется по каждой строке.
canvas:clear()
canvas:clear(tty.style():background("#1a1a1a"):render(" "))
Возвращает: boolean
canvas:put(x, y, text, width?)
Размещает одну стилизованную строку в позиции x, y с отсчётом от единицы и обрезает её до width ячеек (по умолчанию — ширина canvas). Координаты могут быть отрицательными или выходить за край; размещение обрезается, а не отклоняется. Перевод строки завершает строку, поэтому для многострочного содержимого используйте put_rows.
canvas:put(3, 1, tty.style():bold():render("Title"), 40)
Возвращает: boolean
canvas:put_rows(x, y, rows, width?)
Размещает массив стилизованных строк начиная с x, y, по одной строке вниз. Каждый элемент проверяется до того, как что-либо будет нарисовано.
canvas:put_rows(2, 2, child_rows, inner_width)
Возвращает: boolean
canvas:rows()
Рендерит полный массив строк, готовый для surface:present.
Возвращает: string[]
Viewport
Viewport — виртуальный терминальный порт. Создающий процесс становится его первым зрителем; процесс, допущенный по его гранту, — его производителем.
tty.viewport(options?)
local view, err = tty.viewport({
width = 80,
height = 24,
page = {foreground = "#e0def4", background = "#191724"},
})
| Опция | Тип | По умолчанию | Описание |
|---|---|---|---|
width |
number | 80 | Колонки, от 1 до 65535 |
height |
number | 24 | Строки, от 1 до 65535 |
page |
table | нет | Непрозрачные значения цветов переднего и заднего плана по умолчанию в формате #RRGGBB |
Площадь ограничена 262 144 ячейками.
Возвращает: Viewport, error
tty.attach(handle)
Добавляет ещё одного локального зрителя к существующему viewport. Хэндл даёт право просмотра, но не владение представлением, и недействителен на другой ноде.
local view, err = tty.attach(handle)
Возвращает: Viewport, error
viewport:grant()
Возвращает одноразовую возможность производителя. Передайте её как опцию spawn terminal:
local grant = assert(view:grant())
local child = assert(process.with_options({terminal = grant})
:spawn_monitored("app:child", "app:workers"))
Допуск потребляет грант транзакционно: отклонённый запуск восстанавливает грант неразрешённым, а процесс, разрешивший порт, потребляет его окончательно. Хост без поддержки присоединения терминалов отклоняет spawn, а не игнорирует опцию. См. Процессы.
Возвращает: string, error
viewport:handle()
Возвращает локальный хэндл зрителя для tty.attach.
Возвращает: string
viewport:snapshot(after_revision?)
Читает текущие размеры, строки, курсор и ревизию. С after_revision возвращает nil, если ревизия не изменилась.
local frame = view:snapshot(revision)
if frame then
revision = frame.revision
canvas:put_rows(2, 2, frame.rows, inner_width)
end
Возвращает: snapshot или nil
| Поле | Тип | Описание |
|---|---|---|
revision |
number | Монотонная ревизия этого кадра |
width |
number | Колонки viewport |
height |
number | Строки viewport |
rows |
string[] | Строки, последний раз опубликованные производителем |
cursor |
table | {x, y, visible} в координатах с отсчётом от единицы; отсутствует, пока производитель не опубликует явное состояние курсора |
images |
table[] | Метаданные размещений сохраняемых изображений |
layers |
table[] | Упорядоченные слои представления |
images_omitted |
boolean | Ресурсы изображений существуют, но этот обычный снимок их не сохраняет |
Страница преобразует ячейки с цветами терминала по умолчанию и пропущенные
строки в явные цвета. Создатель может изменить её с помощью
viewport:set_page(page); передача nil восстанавливает исходные строки
производителя. Изменение страницы увеличивает ревизию без необходимости
перерисовки со стороны производителя.
viewport:updates()
Возвращает канал объединённых водяных знаков ревизий. receive() выдаёт номер ревизии; case_receive() сочетается с channel.select.
local updates = assert(view:updates())
Обновления — это ограниченные подсказки, а не журнал событий. Медленный зритель получает только самый свежий водяной знак и должен вызывать snapshot() за состоянием. Представление и изменение размера никогда не блокируются из-за медленного зрителя.
Возвращает: ViewportUpdateChannel, error
viewport:send(event)
Пересылает проверенную запись события производителю. Производитель должен был вызвать tty.start(); иначе вызов завершается ошибкой, а не теряет событие.
assert(view:send(event))
assert(view:send({type = "close"}))
Возвращает: boolean, error
viewport:resize(width, height)
Обновляет геометрию viewport. При изменении размера зрители получают новую ревизию, а производитель — событие resize.
Возвращает: boolean, error
viewport:close()
Отсоединяет только этого зрителя. Закрытие последнего зрителя не убивает живого производителя, а закрытие порта производителя не уничтожает состояние, пока остаются зрители.
Возвращает: boolean, error
viewport:mount(recipient_pid, rights)
Выдаёт привязанную к процессу ссылку для локального или удалённого зрителя.
Права независимы друг от друга и по умолчанию имеют значение false:
local observation = assert(view:mount(agent_pid, {observe = true}))
local control = assert(view:mount(agent_pid, {input = true, resize = true}))
-- В точном процессе-получателе на этой ноде или на аутентифицированном mesh-пире:
local observer = assert(tty.attach(observation))
local controller = assert(tty.attach(control))
Mount привязывается к полному PID получателя и может быть погашен только один
раз. Подключённые зрители не могут создавать гранты производителя или
делегировать дальнейшие mount. Удалённые mount используют возобновляемую
аренду; для переподключения требуется новый mount, и терминальный ввод нельзя
воспроизвести повторно. Используйте viewport:revoke(reference), чтобы
отозвать выданный mount. Закрытие viewport владельца или завершение процесса
владельца отзывает его mount.
viewport:capture()
Атомарно фиксирует ревизию viewport и его сохраняемые ресурсы изображений:
local capture = assert(view:capture())
local snapshot = capture:snapshot()
local image = assert(capture:image(snapshot.images[1].image_id))
assert(capture:close())
Обычный snapshot() не сохраняет байты изображений. Capture сохраняет их до
закрытия; хэндлы изображений, уже полученные из него, остаются независимо
владеемыми.
Типы событий
События — это таблицы с полем type, которое определяет, какие другие поля присутствуют. Координаты отсчитываются от единицы. Те же записи принимает viewport:send().
Событие клавиши
{
type = "key",
key = "a", -- печатный символ или имя клавиши
key_type = "runes", -- "runes" для печатных, или имя специальной клавиши
action = "press", -- "press" или "release"
alt = false,
ctrl = false,
shift = false
}
Событие мыши
Требует tty.mouse(true).
{
type = "mouse",
action = "press", -- "press", "release", "motion", "wheel"
button = "left", -- имя кнопки
x = 10,
y = 5,
alt = false,
ctrl = false,
shift = false
}
Событие изменения размера
{type = "resize", width = 120, height = 40}
Событие старта
Выдаётся один раз после tty.start() с начальными размерами.
{type = "start", width = 120, height = 40}
Событие фокуса
Сообщает о владении клавиатурой.
{type = "focus", focused = true}
Событие видимости
Сообщает, полезна ли перерисовка. Оно не предписывает жизненный цикл приложения или фоновые вычисления.
{type = "visibility", visible = true}
Событие вставки
{type = "paste", text = "pasted content"}
Событие закрытия
Просит производителя завершить работу. Оболочка отправляет его через viewport:send, чтобы запросить корректный выход дочернего процесса.
{type = "close"}
Привязки клавиш
Создавайте переиспользуемые привязки клавиш, которые сопоставляются с событиями клавиш:
local quit = tty.bind({
keys = {"q", "ctrl+c"},
help = {key = "q/ctrl+c", desc = "quit"}
})
-- В цикле событий
if quit:matches(ev) then
break
end
tty.bind(config)
| Поле | Тип | Описание |
|---|---|---|
keys |
string[] | Шаблоны клавиш для сопоставления (например, "a", "ctrl+c", "enter") |
help |
table | Опционально. {key = "...", desc = "..."} для текста справки |
Возвращает: KeyBinding
Методы KeyBinding
| Метод | Возвращает | Описание |
|---|---|---|
matches(event) |
boolean | Проверить, соответствует ли событие клавиши этой привязке |
set_enabled(bool) |
self | Включить или отключить привязку |
is_enabled() |
boolean | Проверить, включена ли привязка |
help() |
table | Возвращает справочную информацию {key, desc} |
Стили
Создавайте стилизованный текстовый вывод с помощью стилизации на базе lipgloss. Все методы стиля возвращают новый стиль (immutable).
local tty = require("tty")
local io = require("io")
local title = tty.style()
:bold()
:foreground("#FF0000")
:padding(0, 1)
local box = tty.style()
:border(tty.borders.ROUNDED)
:border_foreground("#00FF00")
:width(40)
:padding(1, 2)
io.print(box:render(title:render("Hello"), "World"))
tty.style()
Создать новый пустой стиль.
Возвращает: Style
Методы Style
Все методы возвращают новый Style и могут быть зацеплены.
Декорация текста
| Метод | Параметр | Описание |
|---|---|---|
foreground(color) |
string | Цвет текста (hex "#FF0000", ANSI "9" или имя) |
background(color) |
string | Цвет фона |
bold(enable?) |
boolean | Жирный текст (по умолчанию: true) |
italic(enable?) |
boolean | Курсивный текст |
underline(enable?) |
boolean | Подчёркнутый текст |
strikethrough(enable?) |
boolean | Перечёркнутый текст |
faint(enable?) |
boolean | Приглушённый текст |
blink(enable?) |
boolean | Мигающий текст |
reverse(enable?) |
boolean | Поменять местами цвет текста и фона |
Компоновка
| Метод | Параметр | Описание |
|---|---|---|
width(n) |
number | Фиксированная ширина |
height(n) |
number | Фиксированная высота |
max_width(n) |
number | Максимальная ширина |
max_height(n) |
number | Максимальная высота |
padding(...) |
numbers | Внутренний отступ (CSS-стиль: top, right, bottom, left) |
margin(...) |
numbers | Внешний отступ (CSS-стиль) |
align(pos) |
number | Горизонтальное выравнивание |
align_vertical(pos) |
number | Вертикальное выравнивание |
inline(enable?) |
boolean | Inline-режим рендеринга |
Границы
| Метод | Параметр | Описание |
|---|---|---|
border(name, ...) |
string, booleans | Стиль границы, опциональные переключатели по сторонам |
border_foreground(...) |
strings | Цвет(а) границы |
border_background(...) |
strings | Цвет(а) фона границы |
Прочее
| Метод | Описание |
|---|---|
render(...) |
Отрендерить строки с применённым стилем |
copy() |
Создать копию этого стиля |
Константы границ
tty.borders.NORMAL
tty.borders.ROUNDED
tty.borders.THICK
tty.borders.DOUBLE
tty.borders.HIDDEN
Константы выравнивания
tty.align.LEFT -- 0
tty.align.CENTER -- 0.5
tty.align.RIGHT -- 1
Утилиты текста
Функции компоновки и измерения для стилизованного текста. Доступны под tty.text.
Измерение
local w = tty.text.width("hello") -- печатная ширина (с учётом ANSI)
local h = tty.text.height("a\nb\nc") -- количество строк
local w, h = tty.text.size("hello\nworld") -- оба значения
Обрезка
-- Обрезать до печатной ширины, с необязательным хвостом
local head = tty.text.truncate(line, 40)
local head = tty.text.truncate(line, 40, "…")
-- Взять диапазон печатных ячеек [left, right)
local middle = tty.text.cut(line, 10, 30)
Обе функции сохраняют состояние ANSI и границы графем, поэтому стилизованный текст можно обрезать и склеивать, не ломая escape-последовательности. truncate возвращает пустую строку при ширине ноль или меньше; cut возвращает пустую строку, если right не больше left.
Соединение
-- Соединить бок о бок, выровняв по верху
local row = tty.text.join_horizontal(tty.text.position.TOP, left, right)
-- Сложить вертикально, центрировано
local col = tty.text.join_vertical(tty.text.position.CENTER, top, bottom)
Максимальные размеры
local w = tty.text.max_width({"short", "a longer string"}) -- самое широкое
local h = tty.text.max_height({"one\ntwo", "single"}) -- самое высокое
Размещение
Поместить строку внутри области заданных размеров:
-- Центрировать в области 80x24
local out = tty.text.place(80, 24, tty.text.position.CENTER, tty.text.position.CENTER, content)
-- Только горизонтально
local out = tty.text.place_horizontal(80, tty.text.position.RIGHT, content)
-- Только вертикально
local out = tty.text.place_vertical(24, tty.text.position.BOTTOM, content)
Константы позиции
tty.text.position.TOP -- 0
tty.text.position.LEFT -- 0
tty.text.position.CENTER -- 0.5
tty.text.position.BOTTOM -- 1
tty.text.position.RIGHT -- 1
Разрешения
Доступ к физическому терминалу определяется фреймом процесса. Для присоединения
производителя через process.with_options({terminal = grant}) на стороне,
порождающей процесс, требуется process.context. При делегировании viewport
дополнительно проверяются следующие права:
| Действие | Ресурс | Описание |
|---|---|---|
tty.mount |
Хэндл viewport владельца | Выдать привязанный к процессу mount |
tty.observe |
Хэндл viewport владельца | Читать снимки, обновления и capture |
tty.input |
Хэндл viewport владельца | Пересылать события ввода |
tty.resize |
Хэндл viewport владельца | Изменять размер viewport |
См. также
- Terminal UI — сборка оболочки, размещающей дочерний процесс в viewport
- Терминальный I/O — операции stdin/stdout/stderr
- Terminal Host — Конфигурация хоста терминала
- Выполнение команд — PTY-процессы и терминальные сессии
- Процессы — опции spawn, мониторинг, события жизненного цикла