- main: when the environment's locale is not UTF-8 (LANG unset, C/POSIX), switch LC_CTYPE to a UTF-8 locale so ncurses delivers and draws multibyte characters instead of dropping them. - Buffer::Id(): a process-unique id that survives moves and is fresh for copies. The terminal renderer's skip and cursor caches and the reload confirmation now key on it rather than the buffer's address, which is reused after a buffer is closed. - The reload confirmation is cancelled by any other command, like the quit confirmation. - TerminalFrontend redraws when a deferred open starts a prompt or sets a status (e.g. the swap recovery question), not only when a buffer opens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
665 lines
21 KiB
C++
665 lines
21 KiB
C++
#include <clocale>
|
|
#define _XOPEN_SOURCE_EXTENDED 1
|
|
#include <cwchar>
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <cstdlib>
|
|
#include <ncurses.h>
|
|
#include <regex>
|
|
#include <string>
|
|
|
|
#include "TerminalRenderer.h"
|
|
#include "RegexGuard.h"
|
|
#include "TermWidth.h"
|
|
#include "Buffer.h"
|
|
#include "Editor.h"
|
|
#include "Highlight.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
|
|
if (content_rows < 1)
|
|
content_rows = 1;
|
|
|
|
int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
|
|
if (buf) {
|
|
// Per-row access only: Rows() rebuilds a string for every line of the
|
|
// file after each edit, which made every frame O(file size).
|
|
const std::size_t nlines = buf->Nrows();
|
|
std::size_t rowoffs = buf->Rowoffs();
|
|
std::size_t coloffs = buf->Coloffs();
|
|
|
|
const int tabw = 8;
|
|
// Phase 3: prefetch visible viewport highlights (current terminal area)
|
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
|
|
int fr = static_cast<int>(rowoffs);
|
|
int rc = std::max(0, content_rows);
|
|
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
|
|
}
|
|
|
|
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;
|
|
// Compute matches for this line if search highlighting is active
|
|
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
|
|
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
|
|
if (search_mode && li < nlines) {
|
|
std::string sline = buf->GetLineString(li);
|
|
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
|
|
if (ed.PromptActive() && (
|
|
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
|
|
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
|
|
// Long lines are not highlighted: std::regex recursion
|
|
// could overflow the stack (RegexGuard.h).
|
|
if (sline.size() <= kte::kRegexRenderLineLimit) try {
|
|
std::regex rx(ed.SearchQuery());
|
|
kte::ForEachRegexMatch(sline, rx, [&](std::size_t pos, std::size_t len) {
|
|
ranges.emplace_back(pos, pos + len);
|
|
});
|
|
} catch (const std::regex_error &) {
|
|
// ignore invalid patterns here; status shows error
|
|
}
|
|
} else {
|
|
const std::string &q = ed.SearchQuery();
|
|
std::size_t pos = 0;
|
|
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
|
|
ranges.emplace_back(pos, pos + q.size());
|
|
pos += q.size();
|
|
}
|
|
}
|
|
}
|
|
auto is_src_in_hl = [&](std::size_t si) -> bool {
|
|
if (ranges.empty())
|
|
return false;
|
|
// ranges are non-overlapping and ordered by construction
|
|
// linear scan is fine for now
|
|
for (const auto &rg: ranges) {
|
|
if (si < rg.first)
|
|
break;
|
|
if (si >= rg.first && si < rg.second)
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
// Track current-match to optionally emphasize
|
|
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
|
|
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
|
|
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
|
|
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
|
|
|
|
// Mark selection (mark -> cursor), in source coordinates
|
|
bool sel_active = false;
|
|
std::size_t sel_sy = 0, sel_sx = 0, sel_ey = 0, sel_ex = 0;
|
|
if (buf->MarkSet()) {
|
|
sel_sy = buf->MarkCury();
|
|
sel_sx = buf->MarkCurx();
|
|
sel_ey = buf->Cury();
|
|
sel_ex = buf->Curx();
|
|
if (sel_sy > sel_ey || (sel_sy == sel_ey && sel_sx > sel_ex)) {
|
|
std::swap(sel_sy, sel_ey);
|
|
std::swap(sel_sx, sel_ex);
|
|
}
|
|
sel_active = !(sel_sy == sel_ey && sel_sx == sel_ex);
|
|
}
|
|
// Visual-line selection: full-line selection range
|
|
const bool vsel_active = buf->VisualLineActive();
|
|
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
|
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
|
auto is_src_in_mark_sel = [&](std::size_t y, std::size_t sx) -> bool {
|
|
if (!sel_active)
|
|
return false;
|
|
if (y < sel_sy || y > sel_ey)
|
|
return false;
|
|
if (sel_sy == sel_ey)
|
|
return sx >= sel_sx && sx < sel_ex;
|
|
if (y == sel_sy)
|
|
return sx >= sel_sx;
|
|
if (y == sel_ey)
|
|
return sx < sel_ex;
|
|
return true;
|
|
};
|
|
int written = 0;
|
|
if (li < nlines) {
|
|
std::string line = buf->GetLineString(li);
|
|
const bool vsel_on_line = vsel_active && li >= vsel_sy && li <= vsel_ey;
|
|
const std::size_t vsel_spot_src = vsel_on_line
|
|
? std::min(buf->Curx(), line.size())
|
|
: 0;
|
|
const bool vsel_spot_is_eol = vsel_on_line && vsel_spot_src == line.size();
|
|
std::size_t vsel_line_rx = 0;
|
|
if (vsel_spot_is_eol) {
|
|
// Compute the rendered (column) width of the line so we can highlight a
|
|
// single cell at EOL when the spot falls beyond the last character.
|
|
std::size_t rc = 0;
|
|
std::size_t si = 0;
|
|
while (si < line.size()) {
|
|
wchar_t wch = L' ';
|
|
int wch_len = 1;
|
|
std::mbstate_t state = std::mbstate_t();
|
|
size_t res = std::mbrtowc(&wch, &line[si], line.size() - si, &state);
|
|
if (res == (size_t) -1 || res == (size_t) -2) {
|
|
wch = static_cast<unsigned char>(line[si]);
|
|
wch_len = 1;
|
|
} else if (res == 0) {
|
|
wch = L'\0';
|
|
wch_len = 1;
|
|
} else {
|
|
wch_len = static_cast<int>(res);
|
|
}
|
|
if (wch == L'\t') {
|
|
constexpr std::size_t tab_width = 8;
|
|
const std::size_t next_tab = tab_width - (rc % tab_width);
|
|
rc += next_tab;
|
|
} else {
|
|
rc += static_cast<std::size_t>(kte::CellWidth(wch));
|
|
}
|
|
si += static_cast<std::size_t>(wch_len);
|
|
}
|
|
vsel_line_rx = rc;
|
|
}
|
|
src_i = 0;
|
|
render_col = 0;
|
|
// Syntax highlighting: fetch per-line spans (sanitized copy)
|
|
std::vector<kte::HighlightSpan> sane_spans;
|
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
|
HasHighlighter()) {
|
|
kte::LineHighlight lh_val = buf->Highlighter()->GetLine(
|
|
*buf, static_cast<int>(li), buf->Version());
|
|
// Sanitize defensively: clamp to [0, line.size()], ensure end>=start, drop empties
|
|
const std::size_t line_len = line.size();
|
|
sane_spans.reserve(lh_val.spans.size());
|
|
for (const auto &sp: lh_val.spans) {
|
|
int s_raw = sp.col_start;
|
|
int e_raw = sp.col_end;
|
|
if (e_raw < s_raw)
|
|
std::swap(e_raw, s_raw);
|
|
std::size_t s = static_cast<std::size_t>(std::max(
|
|
0, std::min(s_raw, static_cast<int>(line_len))));
|
|
std::size_t e = static_cast<std::size_t>(std::max(
|
|
static_cast<int>(s),
|
|
std::min(e_raw, static_cast<int>(line_len))));
|
|
if (e <= s)
|
|
continue;
|
|
sane_spans.push_back(kte::HighlightSpan{
|
|
static_cast<int>(s), static_cast<int>(e), sp.kind
|
|
});
|
|
}
|
|
std::sort(sane_spans.begin(), sane_spans.end(),
|
|
[](const kte::HighlightSpan &a, const kte::HighlightSpan &b) {
|
|
return a.col_start < b.col_start;
|
|
});
|
|
}
|
|
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
|
if (sane_spans.empty())
|
|
return kte::TokenKind::Default;
|
|
int si = static_cast<int>(src_index);
|
|
for (const auto &sp: sane_spans) {
|
|
if (si < sp.col_start)
|
|
break;
|
|
if (si >= sp.col_start && si < sp.col_end)
|
|
return sp.kind;
|
|
}
|
|
return kte::TokenKind::Default;
|
|
};
|
|
auto token_attr = [&](kte::TokenKind k) -> attr_t {
|
|
switch (k) {
|
|
case kte::TokenKind::Keyword:
|
|
case kte::TokenKind::Type:
|
|
case kte::TokenKind::Constant:
|
|
case kte::TokenKind::Function:
|
|
return A_BOLD;
|
|
case kte::TokenKind::Comment:
|
|
return A_DIM;
|
|
case kte::TokenKind::String:
|
|
case kte::TokenKind::Char:
|
|
case kte::TokenKind::Number:
|
|
return A_UNDERLINE;
|
|
default:
|
|
return 0;
|
|
}
|
|
};
|
|
// Fast-forward to the scroll offset with the draw loop's own skip
|
|
// rules (a tab is skipped only if it ends at or before coloffs,
|
|
// any other character while it starts before coloffs), cached
|
|
// for the row's current content.
|
|
if (coloffs > 0 && src_i == 0) {
|
|
if (skip_cache_.size() < static_cast<std::size_t>(content_rows))
|
|
skip_cache_.resize(static_cast<std::size_t>(content_rows));
|
|
SkipCache &sc = skip_cache_[static_cast<std::size_t>(r)];
|
|
if (!(sc.valid && sc.buf_id == buf->Id() && sc.version == buf->Version() && sc.row == li &&
|
|
sc.coloffs == coloffs)) {
|
|
std::size_t s = 0, c = 0;
|
|
while (s < line.size()) {
|
|
std::mbstate_t st = std::mbstate_t();
|
|
wchar_t w = 0;
|
|
std::size_t n = std::mbrtowc(&w, &line[s], line.size() - s, &st);
|
|
if (n == (size_t) -1 || n == (size_t) -2) {
|
|
w = static_cast<unsigned char>(line[s]);
|
|
n = 1;
|
|
} else if (n == 0) {
|
|
w = L'\0';
|
|
n = 1;
|
|
}
|
|
if (w == L'\t') {
|
|
const std::size_t next_tab = tabw - (c % tabw);
|
|
if (c + next_tab > coloffs)
|
|
break;
|
|
c += next_tab;
|
|
} else {
|
|
if (c >= coloffs)
|
|
break;
|
|
c += static_cast<std::size_t>(kte::CellWidth(w));
|
|
}
|
|
s += n;
|
|
}
|
|
sc = SkipCache{buf->Id(), buf->Version(), li, coloffs, s, c, true};
|
|
}
|
|
src_i = sc.src;
|
|
render_col = sc.col;
|
|
}
|
|
while (written < cols) {
|
|
bool from_src = false;
|
|
wchar_t wch = L' ';
|
|
int wch_len = 1;
|
|
int disp_w = 1;
|
|
|
|
if (src_i < line.size()) {
|
|
// Decode UTF-8
|
|
std::mbstate_t state = std::mbstate_t();
|
|
size_t res = std::mbrtowc(
|
|
&wch, &line[src_i], line.size() - src_i, &state);
|
|
if (res == (size_t) -1 || res == (size_t) -2) {
|
|
// Invalid or incomplete; treat as single byte
|
|
wch = static_cast<unsigned char>(line[src_i]);
|
|
wch_len = 1;
|
|
} else if (res == 0) {
|
|
wch = L'\0';
|
|
wch_len = 1;
|
|
} else {
|
|
wch_len = static_cast<int>(res);
|
|
}
|
|
|
|
if (wch == L'\t') {
|
|
std::size_t next_tab = tabw - (render_col % tabw);
|
|
if (render_col + next_tab <= coloffs) {
|
|
render_col += next_tab;
|
|
src_i += wch_len;
|
|
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_mark = is_src_in_mark_sel(li, src_i);
|
|
bool in_vsel =
|
|
vsel_on_line && !vsel_spot_is_eol && src_i ==
|
|
vsel_spot_src;
|
|
bool in_sel = in_mark || in_vsel;
|
|
bool in_hl = search_mode && is_src_in_hl(src_i);
|
|
bool in_cur =
|
|
has_current && li == cur_my && src_i >= cur_mx
|
|
&&
|
|
src_i < cur_mend;
|
|
attr_t a = A_NORMAL;
|
|
a |= token_attr(token_at(src_i));
|
|
if (in_sel) {
|
|
a |= A_REVERSE;
|
|
} else {
|
|
if (in_hl)
|
|
a |= A_STANDOUT;
|
|
if (in_cur)
|
|
a |= A_BOLD;
|
|
}
|
|
attrset(a);
|
|
addch(' ');
|
|
++written;
|
|
++render_col;
|
|
--next_tab;
|
|
}
|
|
src_i += wch_len;
|
|
continue;
|
|
} else {
|
|
// normal char (controls are drawn as ^X or '?')
|
|
disp_w = kte::CellWidth(wch);
|
|
|
|
if (render_col < coloffs) {
|
|
render_col += disp_w;
|
|
src_i += wch_len;
|
|
continue;
|
|
}
|
|
from_src = true;
|
|
}
|
|
} else {
|
|
// beyond EOL, fill spaces
|
|
wch = L' ';
|
|
wch_len = 1;
|
|
disp_w = 1;
|
|
from_src = false;
|
|
}
|
|
|
|
if (written + disp_w > cols) {
|
|
// would overflow, just break
|
|
break;
|
|
}
|
|
|
|
bool in_mark = from_src && is_src_in_mark_sel(li, src_i);
|
|
bool in_vsel = false;
|
|
if (vsel_on_line) {
|
|
if (from_src) {
|
|
in_vsel = !vsel_spot_is_eol && src_i == vsel_spot_src;
|
|
} else {
|
|
in_vsel = vsel_spot_is_eol && render_col == vsel_line_rx;
|
|
}
|
|
}
|
|
bool in_sel = in_mark || in_vsel;
|
|
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
|
|
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx &&
|
|
src_i < cur_mend;
|
|
attr_t a = A_NORMAL;
|
|
if (from_src)
|
|
a |= token_attr(token_at(src_i));
|
|
if (in_sel) {
|
|
a |= A_REVERSE;
|
|
} else {
|
|
if (in_hl)
|
|
a |= A_STANDOUT;
|
|
if (in_cur)
|
|
a |= A_BOLD;
|
|
}
|
|
attrset(a);
|
|
|
|
if (from_src && kte::IsCaretControl(wch)) {
|
|
addch('^');
|
|
addch(wch == 0x7f ? '?' : static_cast<chtype>(wch + 0x40));
|
|
} else if (from_src && kte::IsC1Control(wch)) {
|
|
addch('?');
|
|
} else if (from_src) {
|
|
cchar_t cch;
|
|
wchar_t warr[2] = {wch, L'\0'};
|
|
setcchar(&cch, warr, 0, 0, nullptr);
|
|
add_wch(&cch);
|
|
} else {
|
|
addch(' ');
|
|
}
|
|
|
|
written += disp_w;
|
|
render_col += disp_w;
|
|
if (from_src)
|
|
src_i += wch_len;
|
|
if (src_i >= line.size() && written >= cols)
|
|
break;
|
|
}
|
|
}
|
|
attrset(A_NORMAL);
|
|
clrtoeol();
|
|
}
|
|
|
|
// Place terminal cursor at logical position accounting for tabs and coloffs.
|
|
// Recompute the rendered X using the same logic as the drawing loop to avoid
|
|
// any drift between the command-layer computation and the terminal renderer.
|
|
std::size_t cy = buf->Cury();
|
|
std::size_t cx = buf->Curx();
|
|
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
|
std::size_t rx_recomputed = 0;
|
|
if (cy < nlines && cursor_cache_.valid && cursor_cache_.buf_id == buf->Id() &&
|
|
cursor_cache_.version == buf->Version() && cursor_cache_.row == cy && cursor_cache_.cx == cx) {
|
|
rx_recomputed = cursor_cache_.rx;
|
|
} else if (cy < nlines) {
|
|
const std::string line_for_cursor = buf->GetLineString(cy);
|
|
std::size_t src_i_cur = 0;
|
|
std::size_t render_col_cur = 0;
|
|
while (src_i_cur < line_for_cursor.size() && src_i_cur < cx) {
|
|
std::mbstate_t state = std::mbstate_t();
|
|
wchar_t wch;
|
|
size_t res = std::mbrtowc(
|
|
&wch, &line_for_cursor[src_i_cur], line_for_cursor.size() - src_i_cur,
|
|
&state);
|
|
|
|
// Same decoding and widths as the drawing loop above.
|
|
std::size_t len = res;
|
|
if (res == (size_t) -1 || res == (size_t) -2) {
|
|
wch = static_cast<unsigned char>(line_for_cursor[src_i_cur]);
|
|
len = 1;
|
|
} else if (res == 0) {
|
|
wch = L'\0';
|
|
len = 1;
|
|
}
|
|
if (wch == L'\t') {
|
|
std::size_t next_tab = tabw - (render_col_cur % tabw);
|
|
render_col_cur += next_tab;
|
|
} else {
|
|
render_col_cur += static_cast<std::size_t>(kte::CellWidth(wch));
|
|
}
|
|
src_i_cur += len;
|
|
}
|
|
rx_recomputed = render_col_cur;
|
|
cursor_cache_ = CursorCache{buf->Id(), buf->Version(), cy, cx, rx_recomputed, true};
|
|
}
|
|
int cur_x = static_cast<int>(rx_recomputed) - 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)
|
|
move(rows - 1, 0);
|
|
attron(A_REVERSE);
|
|
|
|
// Fill the status line with spaces first
|
|
for (int i = 0; i < cols; ++i)
|
|
addch(' ');
|
|
|
|
// If a prompt is active, replace the status bar with the full prompt text
|
|
if (ed.PromptActive()) {
|
|
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
|
|
std::string label = ed.PromptLabel();
|
|
std::string ptext = ed.PromptText();
|
|
auto kind = ed.CurrentPromptKind();
|
|
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
|
|
kind == Editor::PromptKind::Chdir) {
|
|
const char *home_c = std::getenv("HOME");
|
|
if (home_c && *home_c) {
|
|
std::string home(home_c);
|
|
// Ensure we match only at the start
|
|
if (ptext.rfind(home, 0) == 0) {
|
|
std::string rest = ptext.substr(home.size());
|
|
if (rest.empty())
|
|
ptext = "~";
|
|
else if (rest[0] == '/' || rest[0] == '\\')
|
|
ptext = std::string("~") + rest;
|
|
}
|
|
}
|
|
}
|
|
// Prefer keeping the tail of the filename visible when it exceeds the window
|
|
std::string msg;
|
|
if (kind == Editor::PromptKind::Command) {
|
|
msg = ": ";
|
|
} else if (!label.empty()) {
|
|
msg = label + ": ";
|
|
}
|
|
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
|
|
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
|
|
Editor::PromptKind::Chdir) && cols > 0) {
|
|
int avail = cols - static_cast<int>(msg.size());
|
|
if (avail <= 0) {
|
|
// No room for label; fall back to showing the rightmost portion of the whole string
|
|
std::string whole = msg + ptext;
|
|
if ((int) whole.size() > cols)
|
|
whole = whole.substr(whole.size() - cols);
|
|
msg = whole;
|
|
} else {
|
|
if ((int) ptext.size() > avail) {
|
|
ptext = ptext.substr(ptext.size() - avail);
|
|
}
|
|
msg += ptext;
|
|
}
|
|
} else {
|
|
// Non-file prompts: simple concatenation and clip by terminal
|
|
msg += ptext;
|
|
}
|
|
|
|
// Draw left-aligned, clipped to width
|
|
if (!msg.empty())
|
|
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
|
|
|
|
// End status rendering for prompt mode
|
|
attroff(A_REVERSE);
|
|
// Restore logical cursor position in content area
|
|
if (saved_cur_y >= 0 && saved_cur_x >= 0)
|
|
move(saved_cur_y, saved_cur_x);
|
|
return;
|
|
}
|
|
|
|
// 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 += " ";
|
|
// Insert buffer position prefix "[x/N] " before filename
|
|
{
|
|
std::size_t total = ed.BufferCount();
|
|
if (total > 0) {
|
|
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // human-friendly 1-based
|
|
left += "[";
|
|
left += std::to_string(static_cast<unsigned long long>(idx1));
|
|
left += "/";
|
|
left += std::to_string(static_cast<unsigned long long>(total));
|
|
left += "] ";
|
|
}
|
|
}
|
|
left += fname;
|
|
if (b && b->Dirty())
|
|
left += " *";
|
|
// Append read-only indicator
|
|
if (b && b->IsReadOnly())
|
|
left += " [RO]";
|
|
// Append total line count as "<n>L"
|
|
if (b) {
|
|
unsigned long lcount = static_cast<unsigned long>(b->Nrows());
|
|
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();
|
|
}
|