This commit is contained in:
KoDer 2026-10-02 00:57:40 +07:00
parent a081d4d7e8
commit ecef2f41af
8 changed files with 519 additions and 210 deletions

View File

@ -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)

Binary file not shown.

View File

@ -363,6 +363,7 @@ static int cmd_tui(const Args& a)
{
tui.update();
}
std::cout << "\033[2J\033[H" << std::flush;
return 0;
}

View File

@ -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<int> xb(B * block), yb(B * block);

View File

@ -1,149 +0,0 @@
#include "tui.h"
#include <iostream>
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<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;
}
}

View File

@ -1,92 +1,110 @@
#ifndef XENITH_TUI_H
#define XENITH_TUI_H
#include <cstdint>
#include <iostream>
#include <memory>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/select.h>
#include <termios.h>
#include <vector>
#include <string>
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<std::unique_ptr<SplitDesc>> 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<std::vector<TuiCell>>& grid, int offset_x, int offset_y, int w, int h, border_charset charset, const SplitDesc* split);
static void 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);
static void draw_border_to_buffer(std::vector<std::vector<TuiCell>>& grid, int w, int h, border_charset charset);
};
}
#endif

51
xenith/ui/tui/mouse.cpp Normal file
View File

@ -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<uint8_t>(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; }
}

382
xenith/ui/tui/tui.cpp Normal file
View File

@ -0,0 +1,382 @@
#include "../tui.h"
#include <chrono>
#include <csignal>
#include <cstring>
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);
}
}