Files
kte/TerminalRenderer.cc
Kyle Isom e4cd4877cc Introduce file picker and GUI configuration with enhancements.
- Add visual file picker for GUI with toggle support.
- Introduce `GUIConfig` class for loading GUI settings from configuration file.
- Refactor window initialization to support dynamic sizing based on configuration.
- Add macOS-specific handling for fullscreen behavior.
- Improve header inclusion order and minor code cleanup.
2025-11-30 18:35:12 -08:00

263 lines
7.0 KiB
C++

#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <ncurses.h>
#include <string>
#include "TerminalRenderer.h"
#include "Buffer.h"
#include "Editor.h"
// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
# define KTE_VERSION_STR "dev"
#endif
TerminalRenderer::TerminalRenderer() = default;
TerminalRenderer::~TerminalRenderer() = default;
void
TerminalRenderer::Draw(Editor &ed)
{
// Determine terminal size and keep editor dimensions in sync
int rows, cols;
getmaxyx(stdscr, rows, cols);
// Clear screen
erase();
const Buffer *buf = ed.CurrentBuffer();
int content_rows = rows - 1; // last line is status
int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
if (buf) {
const auto &lines = buf->Rows();
std::size_t rowoffs = buf->Rowoffs();
std::size_t coloffs = buf->Coloffs();
const int tabw = 8;
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
bool do_hl = ed.SearchActive() && li == ed.SearchMatchY() && ed.SearchMatchLen() > 0;
std::size_t mx = do_hl ? ed.SearchMatchX() : 0;
std::size_t mlen = do_hl ? ed.SearchMatchLen() : 0;
bool hl_on = false;
int written = 0;
if (li < lines.size()) {
const std::string &line = lines[li];
src_i = 0;
render_col = 0;
while (written < cols) {
char ch = ' ';
bool from_src = false;
if (src_i < line.size()) {
unsigned char c = static_cast<unsigned char>(line[src_i]);
if (c == '\t') {
std::size_t next_tab = tabw - (render_col % tabw);
if (render_col + next_tab <= coloffs) {
render_col += next_tab;
++src_i;
continue;
}
// Emit spaces for tab
if (render_col < coloffs) {
// skip to coloffs
std::size_t to_skip = std::min<std::size_t>(
next_tab, coloffs - render_col);
render_col += to_skip;
next_tab -= to_skip;
}
// Now render visible spaces
while (next_tab > 0 && written < cols) {
bool in_hl = do_hl && src_i >= mx && src_i < mx + mlen;
// highlight by source index
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
addch(' ');
++written;
++render_col;
--next_tab;
}
++src_i;
continue;
} else {
// normal char
if (render_col < coloffs) {
++render_col;
++src_i;
continue;
}
ch = static_cast<char>(c);
from_src = true;
}
} else {
// beyond EOL, fill spaces
ch = ' ';
from_src = false;
}
if (do_hl) {
bool in_hl = from_src && src_i >= mx && src_i < mx + mlen;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
}
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
if (from_src)
++src_i;
if (src_i >= line.size() && written >= cols)
break;
}
}
if (hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
clrtoeol();
}
// Place terminal cursor at logical position accounting for tabs and coloffs
std::size_t cy = buf->Cury();
std::size_t rx = buf->Rx(); // render x computed by command layer
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
int cur_x = static_cast<int>(rx) - static_cast<int>(buf->Coloffs());
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
// remember where to leave the terminal cursor after status is drawn
saved_cur_y = cur_y;
saved_cur_x = cur_x;
move(cur_y, cur_x);
}
} else {
mvaddstr(0, 0, "[no buffer]");
}
// Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark
move(rows - 1, 0);
attron(A_REVERSE);
// Fill the status line with spaces first
for (int i = 0; i < cols; ++i)
addch(' ');
// Build left segment
std::string left;
{
const char *app = "kte";
left.reserve(256);
left += app;
left += " ";
left += KTE_VERSION_STR; // already includes leading 'v'
const Buffer *b = buf;
std::string fname;
if (b) {
try {
fname = ed.DisplayNameFor(*b);
} catch (...) {
fname = b->Filename();
try {
fname = std::filesystem::path(fname).filename().string();
} catch (...) {}
}
} else {
fname = "[no name]";
}
left += " ";
left += fname;
if (b && b->Dirty())
left += " *";
// Append total line count as "<n>L"
if (b) {
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());
left += " ";
left += std::to_string(lcount);
left += "L";
}
}
// Build right segment (cursor and mark)
std::string right;
{
int row1 = 0, col1 = 0;
int mrow1 = 0, mcol1 = 0;
bool have_mark = false;
if (buf) {
row1 = static_cast<int>(buf->Cury()) + 1;
col1 = static_cast<int>(buf->Curx()) + 1;
if (buf->MarkSet()) {
have_mark = true;
mrow1 = static_cast<int>(buf->MarkCury()) + 1;
mcol1 = static_cast<int>(buf->MarkCurx()) + 1;
}
}
char rbuf[128];
if (have_mark)
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
else
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
right = rbuf;
}
// Compute placements with truncation rules: prioritize left and right; middle gets remaining
int rlen = static_cast<int>(right.size());
if (rlen > cols) {
// Hard clip right if too long
right = right.substr(static_cast<std::size_t>(rlen - cols), static_cast<std::size_t>(cols));
rlen = cols;
}
int left_max = std::max(0, cols - rlen - 1); // leave at least 1 space between left and right areas
int llen = static_cast<int>(left.size());
if (llen > left_max)
llen = left_max;
// Draw left
if (llen > 0)
mvaddnstr(rows - 1, 0, left.c_str(), llen);
// Draw right, flush to end
int rstart = std::max(0, cols - rlen);
if (rlen > 0)
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
// Middle message
const std::string &msg = ed.Status();
if (!msg.empty()) {
int mid_start = llen + 1; // one space after left
int mid_end = rstart - 1; // one space before right
if (mid_end >= mid_start) {
int avail = mid_end - mid_start + 1;
int mlen = static_cast<int>(msg.size());
int mdraw = std::min(avail, mlen);
int mstart = mid_start + std::max(0, (avail - mdraw) / 2); // center within middle area
mvaddnstr(rows - 1, mstart, msg.c_str(), mdraw);
}
}
attroff(A_REVERSE);
// Restore terminal cursor to the content position so a visible caret
// remains in the editing area (not on the status line).
if (saved_cur_y >= 0 && saved_cur_x >= 0) {
move(saved_cur_y, saved_cur_x);
}
refresh();
}