diff --git a/CMakeLists.txt b/CMakeLists.txt index c2b9971..44e0d98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,8 +31,9 @@ set(SOURCES xenith/ollama_api/handler.h xenith/ollama_api/server.h xenith/ollama_api/server.cpp - xenith/ui/tui.cpp + xenith/ui/tui/tui.cpp xenith/ui/tui.h + xenith/ui/tui/mouse.cpp ) # Заголовочные файлы (для удобства в IDE) diff --git a/bin/Xenith b/bin/Xenith index 047ec56..29b2739 100755 Binary files a/bin/Xenith and b/bin/Xenith differ diff --git a/main.cpp b/main.cpp index 6ff01d5..ae27dec 100644 --- a/main.cpp +++ b/main.cpp @@ -363,6 +363,7 @@ static int cmd_tui(const Args& a) { tui.update(); } + std::cout << "\033[2J\033[H" << std::flush; return 0; } diff --git a/xenith/core/trainer.cpp b/xenith/core/trainer.cpp index 3c7b5e3..5afb580 100644 --- a/xenith/core/trainer.cpp +++ b/xenith/core/trainer.cpp @@ -62,7 +62,7 @@ TrainStats train_model(Params& p, const Tokenizer& tok, const std::string& corpu val_ids.assign(ids.end() - tc.val_tokens, ids.end()); ids.resize(ids.size() - tc.val_tokens); has_val = true; - fprintf(stderr, "валидация: %zu токенов\n", val_ids.size()); + fprintf(stderr, "валидация: %zu токенов\n\033[?25l", val_ids.size()); } std::vector xb(B * block), yb(B * block); diff --git a/xenith/ui/tui.cpp b/xenith/ui/tui.cpp deleted file mode 100644 index 7acd208..0000000 --- a/xenith/ui/tui.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "tui.h" - -#include - -namespace ui -{ - Tui::Tui() - { - ConsoleSize cs = get_console_size(); - int panel_w = (cs.width / 100) * 27; - std::cout << "Ширина (колонок): " << cs.width << "\n"; - std::cout << "Высота (строк): " << cs.height << "\n"; - clear(); - hide_cursor(); - set_foreground(Color::White); - apply_color(); - draw_border((cs.width / 100) * 27, cs.height, border_clip); - draw_border(cs.width - ((cs.width / 100) * 27), cs.height, border_clip, (cs.width / 100) * 27); - 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); - //std::cout << "\033[?1000h\033[?1002h\033[?1006h" << std::flush; - } - - Tui::~Tui() - = default; - - void Tui::update() - { - - } - - bool Tui::is_running() const - { - return running; - } - - Tui::ConsoleSize Tui::get_console_size() - { - ConsoleSize size; - winsize w; - - if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) { - size.width = w.ws_col; - size.height = w.ws_row; - } - - return size; - } - - 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(clr); - } - - void Tui::set_foreground(Color clr) - { - foreground = static_cast(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(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; - } - - -} diff --git a/xenith/ui/tui.h b/xenith/ui/tui.h index 75539f2..206e452 100644 --- a/xenith/ui/tui.h +++ b/xenith/ui/tui.h @@ -1,92 +1,110 @@ #ifndef XENITH_TUI_H #define XENITH_TUI_H +#include #include +#include #include #include +#include +#include +#include +#include namespace ui { + class Mouse + { + private: + struct pos_t { int x; int y; }; + inline static bool btn0 = false; + inline static bool btn1 = false; + inline static bool btn2 = false; + inline static int scroll = 0; + inline static pos_t pos = {}; + public: + static bool is_pressed(int index_btn = 0); + static int get_scroll(); + static void reset_scroll(); + static void parse_ansi(char buf[]); + static pos_t get_pos(); + }; + class Tui { public: enum class Color : int { - DefaultText = 39, - DefaultBg = 49, - Black = 30, - Red = 31, - Green = 32, - Yellow = 33, - Blue = 34, - Magenta = 35, - Cyan = 36, - White = 37 + DefaultText = 39, DefaultBg = 49, Black = 30, Red = 31, + Green = 32, Yellow = 33, Blue = 34, Magenta = 35, + Cyan = 36, White = 37 }; Tui(); ~Tui(); static void update(); bool is_running() const; private: + inline static Mouse mouse = Mouse(); + inline static std::string chat_input = ""; + inline static bool cursor = false; + inline static uint32_t frame_count = 0; int background = 49; int foreground = 39; - bool running = true; - struct ConsoleSize { - int width = 80; - int height = 24; + inline static bool running = true; + inline static int width = 80; + inline static int height = 24; + + struct SplitDesc { + enum Type { VERTICAL, HORIZONTAL }; + Type type; + int position; + std::vector> children; + SplitDesc(Type t, int pos) : type(t), position(pos) {} }; - struct border_charset - { - std::string_view top_left; - std::string_view top_right; - std::string_view bottom_left; - std::string_view bottom_right; - std::string_view bottom; - std::string_view top; - std::string_view vertical; - std::string_view bg; + + struct border_charset { + std::string_view top_left, top_right, bottom_left, bottom_right; + std::string_view bottom, top, horizontal, vertical, bg; + std::string_view cross, tee_up, tee_down, tee_left, tee_right; }; + + // Ваша структура, которая теперь будет использоваться по назначению + struct TuiCell { + std::string ch; // UTF-8 последовательность (например, "│" или "┌") + int width; // Ширина на экране (обычно 1) + + TuiCell() : ch(" "), width(1) {} + TuiCell(const std::string& c, int w) : ch(c), width(w) {} + }; + + inline static struct termios orig_termios; inline static const border_charset border_clip { - .top_left = "╭", - .top_right = "╮", - .bottom_left = "╰", - .bottom_right = "╯", - .bottom = "─", - .top = "─", - .vertical = "│", - .bg = " ", + .top_left="╭", .top_right="╮", .bottom_left="╰", .bottom_right="╯", + .bottom="─", .top="─", .horizontal="─", .vertical="│", .bg=" ", + .cross="┼", .tee_up="┴", .tee_down="┬", .tee_left="┤", .tee_right="├" + }; + inline static const border_charset border_ascii { + .top_left="+", .top_right="+", .bottom_left="+", .bottom_right="+", + .bottom="-", .top="-", .vertical="|", .bg=" ", .cross="+", + .tee_up="+", .tee_down="+", .tee_left="+", .tee_right="+" }; inline static const border_charset border_bold { - .top_left = "┏", - .top_right = "┓", - .bottom_left = "┗", - .bottom_right = "┛", - .bottom = "━", - .top = "━", - .vertical = "┃", - .bg = " ", + .top_left="┏", .top_right="┓", .bottom_left="┗", .bottom_right="┛", + .bottom="━", .top="━", .vertical="┃", .bg=" " }; inline static const border_charset border_solid { - .top_left = "▟", - .top_right = "▙", - .bottom_left = "▜", - .bottom_right = "▛", - .bottom = "█", - .top = "█", - .vertical = "█", - .bg = " ", + .top_left="▟", .top_right="▙", .bottom_left="▜", .bottom_right="▛", + .bottom="█", .top="█", .vertical="█", .bg=" " }; inline static const border_charset border_btn { - .top_left = "▟", - .top_right = "▙", - .bottom_left = "▜", - .bottom_right = "▛", - .bottom = "█", - .top = "█", - .vertical = "█", - .bg = "█", + .top_left="▟", .top_right="▙", .bottom_left="▜", .bottom_right="▛", + .bottom="█", .top="█", .vertical="█", .bg="█" }; - static ConsoleSize get_console_size(); + + static void get_console_size(); + static bool wait_for_input_or_timeout(int timeout_ms); void apply_color() const; + static void disable_raw_mode(); + static void enable_raw_mode(); static void move_cursor(int x, int y); static void clear(); static void draw_border(int w, int h, border_charset charset = border_clip, int x = 1, int y = 1); @@ -99,9 +117,14 @@ namespace ui static void move_cursor_y(int row); static void invert_color(); static void draw_button(int x, int y, int w, int h, std::string_view label, border_charset charset = border_solid); - void invert_color_off(); + static void invert_color_off(); + + // --- ОБНОВЛЕННЫЕ СИГНАТУРЫ --- + void draw_split_border(int w, int h, border_charset charset, int x, int y, const SplitDesc* root); + static void apply_splits_recursive(std::vector>& grid, int offset_x, int offset_y, int w, int h, border_charset charset, const SplitDesc* split); + static void apply_single_split(std::vector>& grid, int offset_x, int offset_y, int w, int h, border_charset charset, const SplitDesc* split); + static void draw_border_to_buffer(std::vector>& grid, int w, int h, border_charset charset); }; } - #endif \ No newline at end of file diff --git a/xenith/ui/tui/mouse.cpp b/xenith/ui/tui/mouse.cpp new file mode 100644 index 0000000..7387111 --- /dev/null +++ b/xenith/ui/tui/mouse.cpp @@ -0,0 +1,51 @@ +#include "../tui.h" + +namespace ui +{ + void Mouse::parse_ansi(char buf[]) + { + int button = 0; + char state = '\0'; + if (sscanf(buf + 3, "%d;%d;%d%c", &button, &pos.x, &pos.y, &state) == 4) + { + auto btn_index = static_cast(button); + uint8_t click_type = btn_index & 0xC3; + if (click_type == 0b00000000) + { + btn0 = (state == 'M'); + } + else if (click_type == 0b00000001) + { + btn1 = (state == 'M'); + } + else if (click_type == 0b00000010) + { + btn2 = (state == 'M'); + } + else if (click_type == 0b01000000) + { + scroll++; + } + else if (click_type == 0b01000001) + { + scroll--; + } + } + } + + + bool Mouse::is_pressed(int index_btn) + { + switch (index_btn) + { + case 0: return btn0; + case 1: return btn1; + case 2: return btn2; + default: return false; + } + } + + void Mouse::reset_scroll() { scroll = 0; } + int Mouse::get_scroll() { return scroll; } + Mouse::pos_t Mouse::get_pos() { return pos; } +} diff --git a/xenith/ui/tui/tui.cpp b/xenith/ui/tui/tui.cpp new file mode 100644 index 0000000..6b63397 --- /dev/null +++ b/xenith/ui/tui/tui.cpp @@ -0,0 +1,382 @@ +#include "../tui.h" + +#include +#include +#include + + +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::VERTICAL, (width / 100) * 27); + + root->children.push_back(nullptr); + + auto chat_split = std::make_unique(SplitDesc::HORIZONTAL, height - 3); + chat_split->children.push_back(nullptr); + auto send_split = std::make_unique(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>" < 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(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(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(clr); + } + + void Tui::set_foreground(Color clr) + { + foreground = static_cast(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(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> grid(h, std::vector(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>& 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>& 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>& 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); + } +}