The text mode lie: why modern TUIs are a nightmare for accessibility
Знаете, что меня бесит? Когда разработчики говорят: «Это же терминал — значит, доступно». Типа, раз нет красивых картинок и WebGL, то незрячий пользователь просто возьмёт и прочитает всё идеально.
Нет. Не возьмёт.
Современные текстовые интерфейсы (TUI) превращают жизнь пользователей с нарушениями зрения в ад. И виноваты не баги — виновата сама архитектура.
Миф о «текстовой доступности»
Среди зрячих разработчиков гуляет устойчивый миф: раз приложение работает в терминале — оно по умолчанию доступно. Логика простая: нет графики, нет сложного DOM, нет канваса (холст — область рисования, где можно свободно рисовать любые элементы) WebGL — только чистый ASCII-текст, который программа экранного доступа (специальная программа для незрячих людей, которая читает вслух всё, что происходит на экране) легко распознает.
Но это полная чушь.
Большинство современных TUI часто более враждебны к доступности, чем плохо написанные графические приложения. Инструменты, которые должны улучшать опыт разработчика (DX) — фреймворки типа Ink для JavaScript/React, Bubble Tea для Go или tcell — активно уничтожают опыт слепых пользователей.
Поток против сетки: ключевое различие
Чтобы понять проблему, нужно разделить два понятия, которые часто смешивают: CLI и TUI.
CLI (интерфейс командной строки) работает на модели стандартного ввода/вывода (stdin/stdout — способ общения программы с внешним миром: stdin — откуда она читает команды, stdout — куда выводит результат). Вы вводите команду → система добавляет результат ниже → курсор движется вниз. Всё линейно и хронологично. Для программ экранного доступа типа Speakup (встроенная программа экранного доступа в Linux) это идеально.
TUI (текстовый пользовательский интерфейс) рассматривает окно терминала не как поток текста, а как двумерную сетку символов — пикселей символьной ячейки. Он жертвует временным потоком ради пространственного макета.
МОДЕЛИ РАБОТЫ ТЕРМИНАЛА ───────────────────────── CLI (ПОТОК) TUI (СЕТКА) ───────────── ──────────── ┌─────────────┐ ┌─────────────┐ │ $ ls │ │ ┌─────────┐ │ │ file1.txt │ │ │ Заголовок│ │ │ file2.txt │ │ ├─────────┤ │ │ │ │ │ │ │ │ │ │ ├─────────┤ │ │ │ │ └─────────┘ │ └─────────────┘ └─────────────┘ Линейный вывод Пространственная разметка (экранный (курсор прыгает по всей читатель счастлив) сетке = хаос для читателя)
Кейс: gemini-cli и безумие курсора
Возьмём конкретный пример — gemini-cli, инструмент на Node.js, использующий фреймворк Ink.
Снаружи это простой чат с ИИ. Под капотом Ink пытается reconcil’ить (синхронизировать состояние — когда программа сравнивает, что было и что стало, чтобы обновить экран) дерево React-компонентов в терминальную сетку.
Когда вы используете этот инструмент с Speakup (Linux) или NVDA (бесплатная программа экранного доступа для Windows), приложение не просто «не работает» — оно активно спамит вас.
Фреймворк обрабатывает экран как реактивный канвас: каждое обновление триггерит перерисовку. Когда ИИ «думает», инструмент обновляет таймер или спиннер. Для этого он перемещает аппаратный курсор (физическая мигающая палочка на экране, которая показывает, где вы сейчас печатаете) на место таймера, пишет новое время и возвращает курсор обратно.
Для зрячего пользователя это мгновенно и незаметно. Для пользователя программы экранного доступа вот что вы услышите:
«Responding… Time elapsed 1s… Responding… Time elapsed 2s… [обрывок истории чата]… Responding…»
Курсор телепортируется по всему экрану для обновления индикаторов состояния, спиннеров и истории сообщений. Speakup пытается прочитать всё, что находится под курсором в конкретную миллисекунду. В итоге вы слышите случайные обрывки разговоров вперемешку с обновлениями таймера — сосредоточиться на том, что печатаете, невозможно.
Кошмар с буфером обмена
Допустим, вы как-то справились со Speakup на Linux и хотите поработать через NVDA на Windows — может быть, нужно вставить ошибку, которую вы получаете.
Вы открываете терминал → SSH на Linux-машину → присоединяетесь к screen-сессии (виртуальное «окно» внутри терминала, которое не закрывается, даже если вы отключитесь от сервера) → вставляете текст. Результат? Немедленный крах программы экранного доступа или серьёзная нестабильность системы.
Почему? Каждый раз, когда вы печатаете символ или вставляете текст, приложение триггерит изменение состояния. Фреймворк решает, что нужно перерисовать интерфейс. Поскольку история разговора — часть этого состояния, приложение пытается мгновенно перерисовать или пересчитать макет для тысяч строк текста. Чем больше сообщений в разговоре, тем сильнее это происходит. И никакой Insert+5 не поможет — эта комбинация должна была избегать объявления динамического изменения контента, но здесь бессильна.
Петля лагов
Фреймворки типа Ink, работающие в однопоточных средах (Node.js), страдают от серьёзной деградации производительности, когда история растёт. Если вы вставляете большой блок текста, система должна посчитать diff (алгоритм сравнения двух версий текста, показывает только что изменилось) для тысяч строк. Это вызывает задержку ввода. Вы нажимаете клавишу и ждёте. Ждать можно до 10 секунд, пока один символ отобразится обратно. Система слишком занята вычислением того, как перерисовать экран, чтобы реально обрабатывать ваш ввод.
Почему старые инструменты работают
Зрячие разработчики часто спрашивают: «Если TUI такие плохие, почему вы используете nano, vim или menuconfig?»
Ответ: эти инструменты позволяют полностью скрыть курсор!
В nano или vim удобство работы зависит от отключения функций отслеживания позиции курсора. Если запустить nano с опцией --constantshow или использовать vim без специфической конфигурации, опыт ломается. Когда курсор видим и активно отслеживается, Speakup приоритизирует обновление позиции курсора над эхом символа. Вместо того чтобы услышать букву «а» при печати, вы слышите «Column 2». Вы печатаете «b» и слышите «Column 3».
Эти старые инструменты побеждают, потому что позволяют вам отключить визуальный шум. Можно настроить их, чтобы подавить визуальный курсор или обновления статусной панели, заставив программу экранного доступа полагаться на поток символов вместо шумных координатных обновлений. Современные фреймворки редко предлагают режим «без курсора» — они считают визуальный курсор необходимым элементом.
Инструменты типа menuconfig из ядра Linux работают, потому что они принудительно используют одноколоночный фокус. Даже если есть рамки и заголовки, активная область — вертикальный список. Курсор остаётся привязанным к этому списку. Он не прыгает вниз, чтобы обновить часы, потом наверх, чтобы обновить заголовок. Пространственная сложность достаточно низкая, чтобы программа экранного доступа никогда не «заблудилась».
Irssi — эталон доступных чатов, но не случайно. Irssi был построен более 20 лет назад со специальным движком рендеринга, использующим VT100 Scrolling Regions (аппаратная возможность терминала прокручивать только часть экрана, а не весь целиком). Когда новое сообщение прибывает, Irssi говорит терминальному драйверу: «Определи область прокрутки со строки 1 по 23». Отослал команду: «Прокрутить вверх». Терминал двигает биты. Рисует новый текст снизу этой области. Критически важно, он делает это способом, минимизирующим вмешательство в строку ввода. Он полагается на аппаратные возможности терминала вместо того, чтобы переписывать каждый символ на экране вручную. Современные фреймворки игнорируют эти аппаратные возможности ради «diff’инга» состояния экрана, что требует больше вычислений и создаёт проблемы для доступности.
Опыт Google
Google и мейнтейнеры gemini-cli делают вид, что заботятся о доступности. Слово «делают вид» — ключевое. Если посмотреть на репозиторий, критические регрессии доступности типа Issue #3435 и Issue #11305 оставлены гнить. Там нет дискуссии, дорожной карты, исправления. Ещё хуже — судьба Issue #1553, которая должна была отслеживать эти провалы. Доступность не решили — её замолчали. Закрыли автоматически ботом с типичным отказом:
Hello! As part of our effort to keep our backlog manageable and focus on the most active issues we are tidying up older reports. It looks like this issue hasn’t been active for a while so we are closing it for now.
Это неприемлемо. Закрыть репорт о доступности, потому что мейнтейнеры не трогали его месяцами — это не «уборка», это сокрытие доказательств. Фактически, если баг игнорируют достаточно долго, он перестаёт существовать. Это повышает метрику «Closed Issues» проекта, оставляя фактически софт непригодным для слепых пользователей.
Что делать?
Если вы строите для терминала и заботитесь о доступности, перестаньте использовать декларативные UI-фреймворки, которые обращаются с терминалом как с канвасом. Современный TUI-стек оптимизировал способность разработчика писать React-подобный код за счёт способности машины эффективно рендерить текст. Если вы не можете гарантировать, что ваше приложение позволяет пользователю скрыть курсор, или если вы полагаетесь на агрессивную перерисовку для показа спиннеров и таймеров, вы строите недоступный инструмент. Для слепого пользователя тупой линейный CLI-поток бесконечно лучше, чем «умный» TUI, который лагает, спамит и рассыпает курсор по экрану.
Может, хватит оптимизировать DX за чужой комфорт?
Ссылки
Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.