pager/source/pager.d
2025-11-19 09:42:10 +03:00

291 lines
14 KiB
D
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module pager; // Объявляем модуль с именем pager
// Подключаем необходимые модули
import arsd.terminal; // Библиотека для работы с терминалом (цвета, ввод, альтернативный экран и т.д.)
import std.array : array, appender; // appender — эффективный «строитель» массива, array — преобразование в обычный массив
import std.uni : toLower; // Приведение символа к нижнему регистру
import std.utf : decode; // Декодирование UTF-8 символов (dchar из строки)
import std.conv : to; // Преобразование типов
import std.format : format; // Форматирование строк вроде printf
// Класс, отвечающий за отображение и навигацию по тексту (аналог less/more)
class PagerText {
private:
Terminal* _terminal; // Указатель на объект терминала (передаём по указателю, т.к. он большой)
string[] _lines; // Исходные строки текста (без переносов)
string[] _wrappedLines; // Строки после переноса по ширине терминала
string[] _wrappedTitle; // Заголовок после переноса по ширине
size_t _top; // Индекс первой видимой строки в _wrappedLines
int _width, _height; // Текущие размеры терминала
int _titleHeight; // Сколько строк занимает полностью развёрнутый заголовок
int _effectiveTitleHeight; // Сколько строк заголовка реально показываем (может быть обрезано)
int _statusHeight; // Высота строки статуса внизу (обычно 0 или 1)
size_t _pageSize; // Сколько строк текста помещается на экране (без заголовка и статуса)
string _title; // Заголовок, который показывается сверху синим фоном
// ------------------------------------------------------------------
// Функция переноса строк с учётом ширины терминала и табуляций
// ------------------------------------------------------------------
string[] wrapLines(const string[] src, int width) {
auto result = appender!(string[])(); // Эффективный «строитель» массива строк
if (width <= 0) width = 1; // Защита от нулевой/отрицательной ширины
foreach (line; src) {
if (line.length == 0) { // Пустая строка → просто пустая строка в результате
result.put("");
continue;
}
size_t start = 0; // Начало текущего куска строки
while (start < line.length) {
size_t idx = start; // Текущая позиция в байтах
int cols = 0; // Сколько «колонок» уже занято в текущей выводимой строке
// Проходим по символ за символом, пока не упрёмся в ширину
while (idx < line.length) {
size_t old_idx = idx;
dchar c = decode(line, idx); // Декодируем один Unicode-символ (может быть 1-4 байта)
// Обработка табуляции: таб = 8 колонок минус остаток по модулю 8
int w = 1;
if (c == '\t') {
w = 8 - (cols % 8);
if (w == 0) w = 8;
}
// Если добавление символа выходит за пределы ширины
if (cols + w > width) {
// Если ещё ничего не набрали в этой строке — придётся разорвать посреди символа
if (cols == 0) {
result.put(line[start .. idx]); // Выводим всё, что успели
start = idx;
break;
} else {
// Откатываемся к предыдущему символу и завершаем строку
idx = old_idx;
break;
}
}
cols += w; // Символ помещается — увеличиваем счётчик колонок
}
// Если удалось что-то набрать — добавляем кусок в результат
if (idx > start) {
result.put(line[start .. idx]);
start = idx;
} else {
// Защита от бесконечного цикла (вдруг decode не сдвинул индекс)
decode(line, start);
}
}
}
return result.data; // Преобразуем appender в обычный массив string[]
}
// ------------------------------------------------------------------
// Пересчитываем размеры, переносы строк и позицию прокрутки при изменении окна
// ------------------------------------------------------------------
void recomputeLayout() {
_height = _terminal.height; // Текущая высота терминала
_width = _terminal.width; // Текущая ширина
// Переносим сам текст и заголовок под новую ширину
_wrappedLines = wrapLines(_lines, _width);
_wrappedTitle = wrapLines([_title], _width);
_titleHeight = cast(int)_wrappedTitle.length;
// Если терминал очень маленький — заголовок может не влезть полностью
int maxTitleLines = _height > 0 ? _height : 0;
_effectiveTitleHeight = _titleHeight > maxTitleLines ? maxTitleLines : _titleHeight;
// Оставшееся место после заголовка
int remaining = _height - _effectiveTitleHeight;
_statusHeight = remaining > 0 ? 1 : 0; // Строка статуса внизу (если есть место)
int contentHeight = remaining - _statusHeight; // Место под сам текст
if (contentHeight < 0) contentHeight = 0;
_pageSize = cast(size_t)contentHeight; // Сколько строк текста видно одновременно
// Корректируем позицию прокрутки, чтобы не выйти за границы
if (_pageSize == 0 || _wrappedLines.length <= _pageSize) {
_top = 0;
} else if (_top > _wrappedLines.length - _pageSize) {
_top = _wrappedLines.length - _pageSize;
}
}
public:
// Конструктор: получаем терминал, массив строк и заголовок
this(Terminal* terminal, string[] lines, string title) {
_terminal = terminal;
_lines = lines.dup; // Делаем копию, чтобы не портить оригинал
_title = title;
recomputeLayout(); // Первичный расчёт размеров
}
// ------------------------------------------------------------------
// Полная перерисовка экрана
// ------------------------------------------------------------------
void redraw() {
_terminal.clear(); // Очищаем весь экран
int y = 0;
// === Рисуем заголовок (синий фон, белый текст) ===
if (_height > 0 && _effectiveTitleHeight > 0) {
_terminal.color(Color.white, Color.blue); // Белый на синем
foreach (size_t i; 0 .. _effectiveTitleHeight) {
_terminal.moveTo(0, y + cast(int)i);
_terminal.clearToEndOfLine(); // Очищаем строку
_terminal.write(_wrappedTitle[i]); // Пишем кусок заголовка
}
_terminal.color(Color.DEFAULT, Color.DEFAULT); // Сбрасываем цвета
y += _effectiveTitleHeight; // Сдвигаем «курсор» ниже заголовка
}
// === Рисуем видимую часть текста ===
for (size_t i = 0; i < _pageSize; ++i) {
size_t idx = _top + i; // Номер строки в общем массиве
if (idx >= _wrappedLines.length) break; // Если текст кончился раньше — выходим
_terminal.moveTo(0, y + cast(int)i);
_terminal.write(_wrappedLines[idx]); // Сам текст
_terminal.clearToEndOfLine(); // Очищаем остаток строки (на случай мусора)
}
// === Нижняя статусная строка ===
if (_statusHeight > 0) {
_terminal.moveTo(0, _height - 1); // Переходим на последнюю строку
_terminal.color(Color.white, Color.blue);
_terminal.clearToEndOfLine();
// Вычисляем процент прокрутки
long percent = 0;
if (_wrappedLines.length > 0) {
auto bottom = _top + _pageSize;
if (bottom > _wrappedLines.length) bottom = _wrappedLines.length;
percent = cast(long)(bottom * 100 / _wrappedLines.length);
}
string status = format("Нажмите q для выхода [%d/%d строк %d%%]",
_wrappedLines.length > 0 ? _top + 1 : _top, // первая видимая строка (с 1)
_wrappedLines.length,
percent
);
_terminal.write(status);
_terminal.color(Color.DEFAULT, Color.DEFAULT);
}
_terminal.flush(); // Принудительно выводим всё на экран
}
// Вызывается при изменении размера окна
void onResize() {
recomputeLayout();
recomputeLayout();
redraw();
}
// Обработка нажатий клавиш
// Возвращает true, если нужно завершить работу (нажали q или Esc)
bool onKey(const KeyboardEvent ke) {
if (!ke.pressed) return false; // Отпускание клавиши игнорируем
dchar key = ke.which; // Какой символ/код клавиши
// q или Q — выход
if ((key >= 'A' && key <= 'Z') || (key >= 'a' && key <= 'z')) {
if (toLower(key) == 'q') return true;
}
if (key == KeyboardEvent.Key.escape) return true;
// Если текста нет или экран слишком маленький — просто перерисовываем
if (_wrappedLines.length == 0 || _pageSize == 0) {
redraw();
return false;
}
// Навигация
switch (key) {
case KeyboardEvent.Key.PageDown:
if (_top + _pageSize < _wrappedLines.length) {
_top += _pageSize;
if (_top > _wrappedLines.length - _pageSize)
_top = _wrappedLines.length - _pageSize;
}
break;
case KeyboardEvent.Key.PageUp:
_top = _top < _pageSize ? 0 : _top - _pageSize;
break;
case KeyboardEvent.Key.DownArrow:
if (_top + _pageSize < _wrappedLines.length) ++_top;
break;
case KeyboardEvent.Key.UpArrow:
if (_top > 0) --_top;
break;
case KeyboardEvent.Key.End:
_top = _wrappedLines.length <= _pageSize ? 0 : _wrappedLines.length - _pageSize;
break;
case KeyboardEvent.Key.Home:
_top = 0;
break;
default: // неизвестная клавиша — ничего не делаем
}
redraw(); // После любого перемещения перерисовываем
return false; // Продолжаем работу
}
}
// ==================================================================
// Основной класс-обёртка, который создаёт терминал и запускает цикл
// ==================================================================
class Pager {
private:
Terminal _terminal; // Объект терминала (по значению — он копируется внутри arsd.terminal)
PagerText _pager; // Наш пейджер с логикой отображения
public:
// Конструктор: принимает текст и опциональный заголовок
this(string[] lines, string title = "") {
// Создаём терминал в «клеточном» режиме (поддерживает цвета, альтернативный экран и т.д.)
_terminal = Terminal(ConsoleOutputType.cellular);
// Создаём объект PagerText, передавая ему указатель на терминал
_pager = new PagerText(&_terminal, lines, title);
}
// Основной метод — запускаем пейджер
void render() {
// Переключаемся на альтернативный экран (чтобы после выхода терминал остался чистым)
_terminal.enableAlternateScreen(true);
_terminal.hideCursor(); // Скрываем мигающий курсор
// scope(exit) — гарантированно выполнится при любом выходе из функции
scope (exit) {
_terminal.clear();
_terminal.showCursor();
_terminal.enableAlternateScreen(false);
_terminal.flush();
}
_pager.redraw(); // Первая отрисовка
// Включаем «сырой» режим ввода + отслеживание изменения размера
auto input = RealTimeConsoleInput(&_terminal, ConsoleInputFlags.raw | ConsoleInputFlags.size);
bool running = true;
while (running) {
auto ev = input.nextEvent(); // Ждём следующее событие (клавиша или ресайз)
if (ev.type == InputEvent.Type.KeyboardEvent) {
auto ke = ev.get!(InputEvent.Type.KeyboardEvent);
if (_pager.onKey(ke)) { // Если onKey вернул true — выходим
running = false;
}
} else if (ev.type == InputEvent.Type.SizeChangedEvent) {
// При изменении размера окна пересчитываем всё
_pager.onResize();
}
}
}
}