387 lines
13 KiB
C++
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);
|
|
}
|
|
}
|