init
This commit is contained in:
commit
13a1988243
8 changed files with 358 additions and 0 deletions
291
source/pager.d
Normal file
291
source/pager.d
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue