- C0 controls and DEL were passed to ncurses, which acted on them: a carriage return moved to column 0 and the line padding then erased the text, so every line of a CRLF file drew blank, and a backspace ate the previous character. They are now drawn in caret notation (^M, ^H, ^?) and counted as two cells. C1 controls (and stray bytes 0x80-0x9F, which decode to them; U+009B is CSI) are drawn as '?'. - The width rule lives in TermWidth.h and is shared by the renderer's drawing loop, its cursor placement and the command layer's horizontal scrolling and mouse mapping, which also disagreed on NUL and invalid bytes. - GUI config: columns/rows must be 1..1000 and the font size a finite 4..200 (TOML 'size = inf' and huge values were accepted, leading to UB in float-to-int conversion and int overflow downstream). Verified in tmux: a CRLF file now shows 'first line^M' where the baseline showed a blank line. Audit findings B9 (and the CR/BS case from the follow-up review). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
626 lines
20 KiB
C++
626 lines
20 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());
|
|
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
|
|
it != std::sregex_iterator(); ++it) {
|
|
const auto &m = *it;
|
|
std::size_t sx = static_cast<std::size_t>(m.position());
|
|
std::size_t ex = sx + static_cast<std::size_t>(m.length());
|
|
ranges.emplace_back(sx, ex);
|
|
}
|
|
} 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;
|
|
}
|
|
};
|
|
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) {
|
|
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;
|
|
}
|
|
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();
|
|
}
|