Xenith/xenith/ui/tui/tui.cpp
2026-10-02 19:30:49 +07:00

387 lines
13 KiB
C++

#include "../tui.hpp"
#include <chrono>
#include <csignal>
#include <cstring>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/select.h>
#include <iostream>
namespace ui
{
Tui::Tui()
{
get_console_size();
int panel_w = (width / 100) * 27;
std::cout << "Ширина (колонок): " << width << "\n";
std::cout << "Высота (строк): " << height << "\n";
clear();
hide_cursor();
set_foreground(Color::White);
apply_color();
auto root = std::make_unique<SplitDesc>(SplitDesc::VERTICAL, (width / 100) * 27);
root->children.push_back(nullptr);
auto chat_split = std::make_unique<SplitDesc>(SplitDesc::HORIZONTAL, height - 3);
chat_split->children.push_back(nullptr);
auto send_split = std::make_unique<SplitDesc>(SplitDesc::VERTICAL, (width - (width / 100) * 27) - 11);
chat_split->children.push_back(std::move(send_split));
root->children.push_back(std::move(chat_split));
draw_split_border(width, height, border_clip, 0, 0, root.get());
draw_button(2, 2, panel_w - 2, 3, "Chat", border_clip);
draw_button(2, 2 + 3, panel_w - 2, 3, "Models", border_clip);
draw_button(2, 2 + 6, panel_w - 2, 3, "Settings", border_clip);
draw_button(2, 2 + 9, panel_w - 2, 3, "Server", border_clip);
move_cursor(width - 8, height - 1);
std::cout << "[enter>" <<std::flush;
enable_raw_mode();
}
Tui::~Tui()
= default;
bool Tui::wait_for_input_or_timeout(int timeout_us) {
fd_set fds;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
struct timeval tv;
tv.tv_sec = timeout_us / 1000000;
tv.tv_usec = timeout_us % 1000000;
int retval = select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
return retval > 0;
}
void Tui::disable_raw_mode()
{
std::cout << "\033[?1006l\033[?1002l\033[?1000l" << std::flush;
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}
void Tui::enable_raw_mode() {
tcgetattr(STDIN_FILENO, &orig_termios);
struct termios raw = orig_termios;
std::signal(SIGINT, [](int sig)
{
disable_raw_mode();
show_cursor();
running = false;
});
raw.c_lflag &= ~(ECHO | ICANON);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
std::cout << "\033[?1000h\033[?1002h\033[?1006h" << std::flush;
}
void Tui::update()
{
bool input_changed = false;
if (wait_for_input_or_timeout(10000)) {
char buf[64];
int bytes_read = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (bytes_read > 0) {
buf[bytes_read] = '\0';
int i = 0;
while (i < bytes_read) {
unsigned char c = buf[i];
if (c == '\033') {
if (i + 2 < bytes_read && strncmp(&buf[i], "\033[<", 3) == 0) {
mouse.parse_ansi(&buf[i]);
while (i < bytes_read && buf[i] != 'm' && buf[i] != 'M') {
i++;
}
i++;
continue;
}
i++;
while (i < bytes_read && buf[i] >= 0x40 && buf[i] <= 0x7E) {
i++;
}
}
else if (c == 127 || c == 8) {
if (!chat_input.empty()) {
while (!chat_input.empty() && (static_cast<unsigned char>(chat_input.back()) & 0xC0) == 0x80) {
chat_input.pop_back();
}
if (!chat_input.empty()) {
chat_input.pop_back();
}
input_changed = true;
}
i++;
}
else if (c >= 32) {
chat_input += c;
input_changed = true;
i++;
}
else {
i++;
}
}
}
}
static auto last_blink = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
if (input_changed || std::chrono::duration_cast<std::chrono::milliseconds>(now - last_blink).count() >= 500) {
if (input_changed) {
last_blink = now;
cursor = true;
} else {
cursor = !cursor;
}
last_blink = now;
int input_x = ((width / 100) * 27) + 3;
move_cursor(input_x, height - 1);
std::cout << chat_input << (cursor ? "_ " : " ") << std::flush;
}
}
bool Tui::is_running() const
{
return running;
}
void Tui::get_console_size()
{
winsize w;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
width = w.ws_col;
height = w.ws_row;
}
}
void Tui::apply_color() const
{
std::cout << "\033["<< foreground << ";" << background <<"m" << std::flush;
}
void Tui::move_cursor(int x, int y)
{
std::cout << "\033[" << y << ";" << x << "H" << std::flush;
}
void Tui::clear()
{
std::cout << "\033c" << std::flush;
}
void Tui::draw_border(int w, int h, border_charset charset, int x, int y)
{
move_cursor(x, y);
std::cout << charset.top_left;
for (int q = 0; q < w - 2; q++) std::cout << charset.top;
std::cout << charset.top_right <<"\n";
move_cursor_x(x);
for (int i = 0; i < h - 2; i++)
{
std::cout << charset.vertical;
for (int q = 0; q < w - 2; q++) std::cout << charset.bg;
std::cout << charset.vertical;
std::cout << "\n";
move_cursor_x(x);
}
std::cout << charset.bottom_left;
for (int q = 0; q < w - 2; q++) std::cout << charset.bottom;
std::cout << charset.bottom_right << std::flush;
}
void Tui::hide_cursor()
{
std::cout << "\033[?25l" << std::flush;
}
void Tui::show_cursor()
{
std::cout << "\033[?25h" << std::flush;
}
void Tui::set_background(Color clr)
{
background = static_cast<int>(clr);
}
void Tui::set_foreground(Color clr)
{
foreground = static_cast<int>(clr);
}
void Tui::move_cursor_right(int n)
{
if (n > 0) {
std::cout << "\033[" << n << "C" << std::flush;
}
}
void Tui::move_cursor_x(int col)
{
if (col > 0) {
std::cout << "\033[" << col << "G" << std::flush;
}
}
void Tui::move_cursor_y(int row)
{
if (row > 0) {
std::cout << "\033[" << row << "d" << std::flush;
}
}
void Tui::invert_color()
{
std::cout << "\033[7m" << std::flush;
}
void Tui::draw_button(int x, int y, int w, int h, std::string_view label, border_charset charset)
{
draw_border(w, h, charset, x, y);
int text_y = y + (h / 2);
int text_x = x + ((w - static_cast<int>(label.length())) / 2);
move_cursor(text_x, text_y);
std::cout << label;
std::cout << std::flush;
}
void Tui::invert_color_off()
{
std::cout << "\033[27m" << std::flush;
}
void Tui::draw_split_border(int w, int h, border_charset charset, int x, int y, const SplitDesc* root)
{
// Создаем сетку из TuiCell, инициализированную фоновым символом
std::vector<std::vector<TuiCell>> grid(h, std::vector<TuiCell>(w, TuiCell(std::string(charset.bg), 1)));
draw_border_to_buffer(grid, w, h, charset);
if (root) {
apply_splits_recursive(grid, 0, 0, w, h, charset, root);
}
move_cursor(x, y);
// Посимвольный вывод без искажения UTF-8
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
std::cout << grid[i][j].ch;
}
if (i < h - 1) {
std::cout << "\n";
}
}
std::cout << std::flush;
}
void Tui::apply_splits_recursive(std::vector<std::vector<TuiCell>>& grid,
int offset_x, int offset_y,
int w, int h,
border_charset charset,
const SplitDesc* split)
{
if (!split) return;
apply_single_split(grid, offset_x, offset_y, w, h, charset, split);
if (split->type == SplitDesc::VERTICAL) {
if (split->children.size() >= 1) {
int left_w = split->position;
apply_splits_recursive(grid, offset_x, offset_y, left_w, h, charset, split->children[0].get());
}
if (split->children.size() >= 2) {
int right_w = w - split->position;
apply_splits_recursive(grid, offset_x + split->position, offset_y, right_w, h, charset, split->children[1].get());
}
} else { // HORIZONTAL
if (split->children.size() >= 1) {
int top_h = split->position;
apply_splits_recursive(grid, offset_x, offset_y, w, top_h, charset, split->children[0].get());
}
if (split->children.size() >= 2) {
int bottom_h = h - split->position;
apply_splits_recursive(grid, offset_x, offset_y + split->position, w, bottom_h, charset, split->children[1].get());
}
}
}
void Tui::apply_single_split(std::vector<std::vector<TuiCell>>& grid, int offset_x, int offset_y, int w, int h, border_charset charset, const SplitDesc* split)
{
if (split->type == SplitDesc::VERTICAL) {
int x = offset_x + split->position;
for (int i = 0; i < h; i++) {
int y = offset_y + i;
TuiCell& cell = grid[y][x];
if (i == 0) {
if (cell.ch == charset.top) cell.ch = std::string(charset.tee_down);
} else if (i == h - 1) {
if (cell.ch == charset.bottom) cell.ch = std::string(charset.tee_up);
} else {
if (cell.ch == charset.bg) {
cell.ch = std::string(charset.vertical);
} else if (cell.ch == charset.horizontal || cell.ch == charset.top || cell.ch == charset.bottom) {
cell.ch = std::string(charset.cross);
}
}
}
} else { // HORIZONTAL
int y = offset_y + split->position;
for (int j = 0; j < w; j++) {
int x = offset_x + j;
TuiCell& cell = grid[y][x];
if (j == 0) {
if (cell.ch == charset.vertical) cell.ch = std::string(charset.tee_right);
} else if (j == w - 1) {
if (cell.ch == charset.vertical) cell.ch = std::string(charset.tee_left);
} else {
if (cell.ch == charset.bg) {
// Используем horizontal вместо bottom для семантической корректности горизонтального разделителя
cell.ch = std::string(charset.horizontal);
} else if (cell.ch == charset.vertical) {
cell.ch = std::string(charset.cross);
}
}
}
}
}
void Tui::draw_border_to_buffer(std::vector<std::vector<TuiCell>>& grid, int w, int h, border_charset charset)
{
if (w <= 0 || h <= 0) return;
// Верхняя строка
grid[0][0] = TuiCell(std::string(charset.top_left), 1);
for (int j = 1; j < w - 1; j++) grid[0][j] = TuiCell(std::string(charset.top), 1);
grid[0][w - 1] = TuiCell(std::string(charset.top_right), 1);
// Средние строки
for (int i = 1; i < h - 1; i++) {
grid[i][0] = TuiCell(std::string(charset.vertical), 1);
grid[i][w - 1] = TuiCell(std::string(charset.vertical), 1);
}
// Нижняя строка
grid[h - 1][0] = TuiCell(std::string(charset.bottom_left), 1);
for (int j = 1; j < w - 1; j++) grid[h - 1][j] = TuiCell(std::string(charset.bottom), 1);
grid[h - 1][w - 1] = TuiCell(std::string(charset.bottom_right), 1);
}
}