Various minor bug cleanups.
This commit is contained in:
18
.idea/workspace.xml
generated
18
.idea/workspace.xml
generated
@@ -35,7 +35,23 @@
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/.junie/guidelines.md" beforeDir="false" afterPath="$PROJECT_DIR$/.junie/guidelines.md" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Editor.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIFrontend.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIInputHandler.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/KKeymap.h" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalFrontend.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.h" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalRenderer.h" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.h" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -140,7 +156,7 @@
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1764457173148</updated>
|
||||
<workItem from="1764457174208" duration="20181000" />
|
||||
<workItem from="1764457174208" duration="22207000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
@@ -8,6 +8,8 @@ WordStar/VDE family and emacs. The spiritual parent is `mg(1)`.
|
||||
These guidelines summarize the goals, interfaces, key operations, and current
|
||||
development practices for kte.
|
||||
|
||||
Style note: all code should be formatted with the current CLion C++ style.
|
||||
|
||||
## Goals
|
||||
|
||||
- Keep the core small, fast, and understandable.
|
||||
|
||||
69
Command.cc
69
Command.cc
@@ -296,7 +296,22 @@ cmd_save_as(CommandContext &ctx)
|
||||
static bool
|
||||
cmd_quit(CommandContext &ctx)
|
||||
{
|
||||
// Placeholder: actual app loop should react to this status or a future flag
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
// If a confirmation is already pending, quit now without saving
|
||||
if (ctx.editor.QuitConfirmPending()) {
|
||||
ctx.editor.SetQuitConfirmPending(false);
|
||||
ctx.editor.SetQuitRequested(true);
|
||||
ctx.editor.SetStatus("Quit requested");
|
||||
return true;
|
||||
}
|
||||
// If current buffer exists and is dirty, warn and arm confirmation
|
||||
if (buf && buf->Dirty()) {
|
||||
ctx.editor.SetStatus("Unsaved changes. C-k q to quit without saving");
|
||||
ctx.editor.SetQuitConfirmPending(true);
|
||||
return true;
|
||||
}
|
||||
// Otherwise quit immediately
|
||||
ctx.editor.SetQuitRequested(true);
|
||||
ctx.editor.SetStatus("Quit requested");
|
||||
return true;
|
||||
}
|
||||
@@ -329,6 +344,16 @@ cmd_save_and_quit(CommandContext &ctx)
|
||||
}
|
||||
}
|
||||
ctx.editor.SetStatus("Save and quit requested");
|
||||
ctx.editor.SetQuitRequested(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_quit_now(CommandContext &ctx)
|
||||
{
|
||||
ctx.editor.SetQuitRequested(true);
|
||||
ctx.editor.SetStatus("Quit requested");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1065,24 +1090,7 @@ cmd_word_next(CommandContext &ctx)
|
||||
while (repeat-- > 0) {
|
||||
if (y >= rows.size())
|
||||
break;
|
||||
// Skip whitespace to the right
|
||||
while (y < rows.size()) {
|
||||
if (y >= rows.size())
|
||||
break;
|
||||
if (x < rows[y].size() && std::isspace(static_cast<unsigned char>(rows[y][x]))) {
|
||||
++x;
|
||||
continue;
|
||||
}
|
||||
if (x >= rows[y].size()) {
|
||||
if (y + 1 >= rows.size())
|
||||
break;
|
||||
++y;
|
||||
x = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Skip word characters to the right
|
||||
// First, if currently on a word, skip to its end
|
||||
while (y < rows.size()) {
|
||||
if (x < rows[y].size() && is_word_char(static_cast<unsigned char>(rows[y][x]))) {
|
||||
++x;
|
||||
@@ -1097,6 +1105,23 @@ cmd_word_next(CommandContext &ctx)
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Then, skip any non-word characters (including punctuation and whitespace)
|
||||
while (y < rows.size()) {
|
||||
if (x < rows[y].size()) {
|
||||
unsigned char c = static_cast<unsigned char>(rows[y][x]);
|
||||
if (is_word_char(c))
|
||||
break;
|
||||
++x;
|
||||
continue;
|
||||
}
|
||||
if (x >= rows[y].size()) {
|
||||
if (y + 1 >= rows.size())
|
||||
break;
|
||||
++y;
|
||||
x = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
buf->SetCursor(x, y);
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
@@ -1163,6 +1188,7 @@ InstallDefaultCommands()
|
||||
CommandRegistry::Register({CommandId::Save, "save", "Save current buffer", cmd_save});
|
||||
CommandRegistry::Register({CommandId::SaveAs, "save-as", "Save current buffer as...", cmd_save_as});
|
||||
CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit});
|
||||
CommandRegistry::Register({CommandId::QuitNow, "quit-now", "Quit editor immediately", cmd_quit_now});
|
||||
CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
|
||||
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
|
||||
CommandRegistry::Register(
|
||||
@@ -1205,6 +1231,11 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count)
|
||||
const Command *cmd = CommandRegistry::FindById(id);
|
||||
if (!cmd)
|
||||
return false;
|
||||
// If a quit confirmation was pending and the user invoked something other
|
||||
// than the soft quit again, cancel the pending confirmation.
|
||||
if (ed.QuitConfirmPending() && id != CommandId::Quit && id != CommandId::KPrefix) {
|
||||
ed.SetQuitConfirmPending(false);
|
||||
}
|
||||
CommandContext ctx{ed, arg, count};
|
||||
return cmd->handler ? cmd->handler(ctx) : false;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ enum class CommandId {
|
||||
SaveAs,
|
||||
// Placeholders for future commands from ke's model
|
||||
Quit,
|
||||
QuitNow, // immediate quit, no confirmation
|
||||
SaveAndQuit,
|
||||
Refresh, // force redraw
|
||||
KPrefix, // show "C-k _" prompt in status when entering k-command
|
||||
@@ -91,6 +92,7 @@ void InstallDefaultCommands();
|
||||
// Dispatcher entry points for the input layer
|
||||
// Returns true if the command executed successfully.
|
||||
bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0);
|
||||
|
||||
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||
|
||||
#endif // KTE_COMMAND_H
|
||||
|
||||
@@ -116,6 +116,8 @@ Editor::Reset()
|
||||
msgtm_ = 0;
|
||||
uarg_ = 0;
|
||||
ucount_ = 0;
|
||||
quit_requested_ = false;
|
||||
quit_confirm_pending_ = false;
|
||||
buffers_.clear();
|
||||
curbuf_ = 0;
|
||||
}
|
||||
|
||||
29
Editor.h
29
Editor.h
@@ -111,6 +111,31 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// --- Quit/Exit state ---
|
||||
void SetQuitRequested(bool on)
|
||||
{
|
||||
quit_requested_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool QuitRequested() const
|
||||
{
|
||||
return quit_requested_;
|
||||
}
|
||||
|
||||
|
||||
void SetQuitConfirmPending(bool on)
|
||||
{
|
||||
quit_confirm_pending_ = on;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] bool QuitConfirmPending() const
|
||||
{
|
||||
return quit_confirm_pending_;
|
||||
}
|
||||
|
||||
|
||||
[[nodiscard]] std::time_t StatusTime() const
|
||||
{
|
||||
return msgtm_;
|
||||
@@ -357,6 +382,10 @@ private:
|
||||
std::vector<Buffer> buffers_;
|
||||
std::size_t curbuf_ = 0; // index into buffers_
|
||||
|
||||
// Quit state
|
||||
bool quit_requested_ = false;
|
||||
bool quit_confirm_pending_ = false;
|
||||
|
||||
// Search state
|
||||
bool search_active_ = false;
|
||||
std::string search_query_;
|
||||
|
||||
@@ -101,11 +101,12 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
break;
|
||||
if (mi.hasCommand) {
|
||||
Execute(ed, mi.id, mi.arg, mi.count);
|
||||
if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) {
|
||||
}
|
||||
}
|
||||
|
||||
if (ed.QuitRequested()) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start a new ImGui frame
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
@@ -117,8 +118,10 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float line_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
float ch_w = ImGui::CalcTextSize("M").x;
|
||||
if (line_h <= 0.0f) line_h = 16.0f;
|
||||
if (ch_w <= 0.0f) ch_w = 8.0f;
|
||||
if (line_h <= 0.0f)
|
||||
line_h = 16.0f;
|
||||
if (ch_w <= 0.0f)
|
||||
ch_w = 8.0f;
|
||||
// Prefer ImGui IO display size; fall back to cached SDL window size
|
||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
|
||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
|
||||
@@ -198,4 +201,5 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// No runtime font reload or system font resolution in this simplified build.
|
||||
@@ -43,6 +43,13 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
||||
case SDLK_BACKSPACE:
|
||||
out = {true, CommandId::Backspace, "", 0};
|
||||
return true;
|
||||
case SDLK_TAB:
|
||||
// Insert a literal tab character
|
||||
out.hasCommand = true;
|
||||
out.id = CommandId::InsertText;
|
||||
out.arg = "\t";
|
||||
out.count = 0;
|
||||
return true;
|
||||
case SDLK_RETURN:
|
||||
case SDLK_KP_ENTER:
|
||||
out = {true, CommandId::Newline, "", 0};
|
||||
@@ -56,62 +63,31 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
||||
}
|
||||
|
||||
if (is_ctrl) {
|
||||
switch (key) {
|
||||
case SDLK_k:
|
||||
case SDLK_KP_EQUALS: // treat Ctrl-K
|
||||
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
|
||||
k_prefix = true;
|
||||
out = {true, CommandId::KPrefix, "", 0};
|
||||
return true;
|
||||
case SDLK_n: // C-n: down
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
}
|
||||
// Map other control chords via shared keymap
|
||||
if (key >= SDLK_a && key <= SDLK_z) {
|
||||
int ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
||||
CommandId id;
|
||||
if (KLookupCtrlCommand(ascii_key, id)) {
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
case SDLK_p: // C-p: up
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
case SDLK_f: // C-f: right
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
return true;
|
||||
case SDLK_b: // C-b: left
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
return true;
|
||||
case SDLK_a:
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
return true;
|
||||
case SDLK_e:
|
||||
out = {true, CommandId::MoveEnd, "", 0};
|
||||
return true;
|
||||
case SDLK_g:
|
||||
k_prefix = false;
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
return true;
|
||||
case SDLK_l:
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
return true;
|
||||
case SDLK_s:
|
||||
out = {true, CommandId::FindStart, "", 0};
|
||||
return true;
|
||||
case SDLK_q:
|
||||
out = {true, CommandId::Quit, "", 0};
|
||||
return true;
|
||||
case SDLK_x:
|
||||
out = {true, CommandId::SaveAndQuit, "", 0};
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alt/Meta bindings (ESC f/b equivalent)
|
||||
if (is_alt) {
|
||||
switch (key) {
|
||||
case SDLK_b:
|
||||
out = {true, CommandId::WordPrev, "", 0};
|
||||
if (key >= SDLK_a && key <= SDLK_z) {
|
||||
int ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
||||
CommandId id;
|
||||
if (KLookupEscCommand(ascii_key, id)) {
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
case SDLK_f:
|
||||
out = {true, CommandId::WordNext, "", 0};
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,16 +126,60 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
MappedInput mi;
|
||||
bool produced = false;
|
||||
switch (e.type) {
|
||||
case SDL_KEYDOWN:
|
||||
produced = map_key(e.key.keysym.sym, SDL_Keymod(e.key.keysym.mod), k_prefix_, mi);
|
||||
case SDL_KEYDOWN: {
|
||||
// Remember whether we were in k-prefix before handling this key
|
||||
bool was_k_prefix = k_prefix_;
|
||||
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
||||
const SDL_Keycode key = e.key.keysym.sym;
|
||||
produced = map_key(key, mods, k_prefix_, mi);
|
||||
// Suppress the immediate following SDL_TEXTINPUT only in cases where
|
||||
// SDL would also emit a text input for the same physical keystroke:
|
||||
// - k-prefix printable suffix keys (no Ctrl), and
|
||||
// - Alt/Meta modified printable letters.
|
||||
// Do NOT suppress for non-text keys like Tab/Enter/Backspace/arrows/etc.,
|
||||
// otherwise the next normal character would be dropped.
|
||||
if (produced && mi.hasCommand) {
|
||||
const bool is_ctrl = (mods & KMOD_CTRL) != 0;
|
||||
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||
const bool is_printable_letter = (key >= SDLK_SPACE && key <= SDLK_z);
|
||||
const bool is_non_text_key =
|
||||
key == SDLK_TAB || key == SDLK_RETURN || key == SDLK_KP_ENTER ||
|
||||
key == SDLK_BACKSPACE || key == SDLK_DELETE || key == SDLK_ESCAPE ||
|
||||
key == SDLK_LEFT || key == SDLK_RIGHT || key == SDLK_UP || key == SDLK_DOWN ||
|
||||
key == SDLK_HOME || key == SDLK_END || key == SDLK_PAGEUP || key == SDLK_PAGEDOWN;
|
||||
|
||||
bool should_suppress = false;
|
||||
if (!is_non_text_key) {
|
||||
// k-prefix then a printable key normally generates TEXTINPUT
|
||||
if (was_k_prefix && is_printable_letter && !is_ctrl) {
|
||||
should_suppress = true;
|
||||
}
|
||||
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
||||
if (is_alt && key >= SDLK_a && key <= SDLK_z) {
|
||||
should_suppress = true;
|
||||
}
|
||||
}
|
||||
if (should_suppress) {
|
||||
suppress_text_input_once_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SDL_TEXTINPUT:
|
||||
if (e.text.text[0] != '\0') {
|
||||
// Ignore text input while in k-prefix, or once after a command-producing keydown
|
||||
if (suppress_text_input_once_) {
|
||||
suppress_text_input_once_ = false; // consume suppression
|
||||
produced = true; // consumed input
|
||||
break;
|
||||
}
|
||||
if (!k_prefix_ && e.text.text[0] != '\0') {
|
||||
mi.hasCommand = true;
|
||||
mi.id = CommandId::InsertText;
|
||||
mi.arg = std::string(e.text.text);
|
||||
mi.count = 0;
|
||||
produced = true;
|
||||
} else {
|
||||
produced = true; // consumed while k-prefix is active
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -27,6 +27,9 @@ private:
|
||||
std::mutex mu_;
|
||||
std::queue<MappedInput> q_;
|
||||
bool k_prefix_ = false;
|
||||
// When a printable keydown generated a non-text command, suppress the very next SDL_TEXTINPUT
|
||||
// event produced by SDL for the same keystroke to avoid inserting stray characters.
|
||||
bool suppress_text_input_once_ = false;
|
||||
};
|
||||
|
||||
#endif // KTE_GUI_INPUT_HANDLER_H
|
||||
|
||||
@@ -78,7 +78,8 @@ GUIRenderer::Draw(Editor &ed)
|
||||
float child_h = ImGui::GetWindowHeight();
|
||||
long first_row = static_cast<long>(scroll_y / row_h);
|
||||
long vis_rows = static_cast<long>(child_h / row_h);
|
||||
if (vis_rows < 1) vis_rows = 1;
|
||||
if (vis_rows < 1)
|
||||
vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
|
||||
// A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row
|
||||
@@ -86,7 +87,8 @@ GUIRenderer::Draw(Editor &ed)
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
long new_row = (cyr < first_row) ? first_row : last_row;
|
||||
if (new_row < 0) new_row = 0;
|
||||
if (new_row < 0)
|
||||
new_row = 0;
|
||||
if (new_row >= static_cast<long>(lines.size()))
|
||||
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
|
||||
// Clamp column to line length
|
||||
@@ -114,8 +116,10 @@ GUIRenderer::Draw(Editor &ed)
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
float max_y = ImGui::GetScrollMaxY();
|
||||
if (target < 0.f) target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y) target = max_y;
|
||||
if (target < 0.f)
|
||||
target = 0.f;
|
||||
if (max_y >= 0.f && target > max_y)
|
||||
target = max_y;
|
||||
ImGui::SetScrollY(target);
|
||||
// refresh local variables
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
@@ -216,13 +220,16 @@ GUIRenderer::Draw(Editor &ed)
|
||||
left += KTE_VERSION_STR;
|
||||
std::string fname = buf->Filename();
|
||||
if (!fname.empty()) {
|
||||
try { fname = std::filesystem::path(fname).filename().string(); } catch (...) {}
|
||||
try {
|
||||
fname = std::filesystem::path(fname).filename().string();
|
||||
} catch (...) {}
|
||||
} else {
|
||||
fname = "[no name]";
|
||||
}
|
||||
left += " ";
|
||||
left += fname;
|
||||
if (buf->Dirty()) left += " *";
|
||||
if (buf->Dirty())
|
||||
left += " *";
|
||||
|
||||
// Build right text (cursor/mark)
|
||||
int row1 = static_cast<int>(buf->Cury()) + 1;
|
||||
@@ -231,8 +238,10 @@ GUIRenderer::Draw(Editor &ed)
|
||||
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
||||
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
||||
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);
|
||||
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);
|
||||
std::string right = rbuf;
|
||||
|
||||
// Middle message
|
||||
|
||||
62
KKeymap.cc
62
KKeymap.cc
@@ -13,7 +13,7 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
out = CommandId::SaveAndQuit;
|
||||
return true; // C-k C-x
|
||||
case 'q':
|
||||
out = CommandId::Quit;
|
||||
out = CommandId::QuitNow;
|
||||
return true; // C-k C-q (quit immediately)
|
||||
default:
|
||||
break;
|
||||
@@ -38,3 +38,63 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
auto
|
||||
KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
|
||||
{
|
||||
const int k = KLowerAscii(ascii_key);
|
||||
switch (k) {
|
||||
case 'n':
|
||||
out = CommandId::MoveDown;
|
||||
return true;
|
||||
case 'p':
|
||||
out = CommandId::MoveUp;
|
||||
return true;
|
||||
case 'f':
|
||||
out = CommandId::MoveRight;
|
||||
return true;
|
||||
case 'b':
|
||||
out = CommandId::MoveLeft;
|
||||
return true;
|
||||
case 'a':
|
||||
out = CommandId::MoveHome;
|
||||
return true;
|
||||
case 'e':
|
||||
out = CommandId::MoveEnd;
|
||||
return true;
|
||||
case 's':
|
||||
out = CommandId::FindStart;
|
||||
return true;
|
||||
case 'l':
|
||||
out = CommandId::Refresh;
|
||||
return true;
|
||||
case 'g':
|
||||
out = CommandId::Refresh;
|
||||
return true;
|
||||
case 'x':
|
||||
out = CommandId::SaveAndQuit; // direct C-x mapping (GUI had this)
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
auto
|
||||
KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
||||
{
|
||||
const int k = KLowerAscii(ascii_key);
|
||||
switch (k) {
|
||||
case 'b':
|
||||
out = CommandId::WordPrev;
|
||||
return true;
|
||||
case 'f':
|
||||
out = CommandId::WordNext;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
// Returns true and sets out if a mapping exists; false otherwise.
|
||||
bool KLookupKCommand(int ascii_key, bool ctrl, CommandId &out);
|
||||
|
||||
// Lookup direct Control-chord commands (e.g., C-n, C-p, C-f, ...).
|
||||
// ascii_key should be the lowercase ASCII of the letter (e.g., 'n' for C-n).
|
||||
bool KLookupCtrlCommand(int ascii_key, CommandId &out);
|
||||
|
||||
// Lookup ESC/Meta + key commands (e.g., ESC f/b).
|
||||
// ascii_key should be the lowercase ASCII of the letter.
|
||||
bool KLookupEscCommand(int ascii_key, CommandId &out);
|
||||
|
||||
// Utility: normalize an int keycode to lowercased ASCII if it's in printable range.
|
||||
inline int
|
||||
KLowerAscii(const int key)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <unistd.h>
|
||||
#include <termios.h>
|
||||
#include <ncurses.h>
|
||||
|
||||
#include "Editor.h"
|
||||
@@ -9,6 +10,14 @@
|
||||
bool
|
||||
TerminalFrontend::Init(Editor &ed)
|
||||
{
|
||||
// Ensure Ctrl-S/Ctrl-Q reach the application by disabling XON/XOFF flow control
|
||||
{
|
||||
struct termios tio{};
|
||||
if (tcgetattr(STDIN_FILENO, &tio) == 0) {
|
||||
tio.c_iflag &= static_cast<unsigned long>(~IXON);
|
||||
(void) tcsetattr(STDIN_FILENO, TCSANOW, &tio);
|
||||
}
|
||||
}
|
||||
initscr();
|
||||
cbreak();
|
||||
noecho();
|
||||
@@ -52,15 +61,16 @@ TerminalFrontend::Step(Editor &ed, bool &running)
|
||||
if (input_.Poll(mi)) {
|
||||
if (mi.hasCommand) {
|
||||
Execute(ed, mi.id, mi.arg, mi.count);
|
||||
if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Avoid busy loop
|
||||
usleep(1000);
|
||||
}
|
||||
|
||||
if (ed.QuitRequested()) {
|
||||
running = false;
|
||||
}
|
||||
|
||||
renderer_.Draw(ed);
|
||||
}
|
||||
|
||||
|
||||
@@ -95,38 +95,23 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('L')) {
|
||||
out = {true, CommandId::Refresh, "", 0};
|
||||
// Tab (note: terminals encode Tab and C-i as the same code 9)
|
||||
if (ch == '\t') {
|
||||
k_prefix = false;
|
||||
out.hasCommand = true;
|
||||
out.id = CommandId::InsertText;
|
||||
out.arg = "\t";
|
||||
out.count = 0;
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('S')) {
|
||||
out = {true, CommandId::FindStart, "", 0};
|
||||
// Generic Control-chord lookup (after handling special prefixes/cancel)
|
||||
if (ch >= 1 && ch <= 26) {
|
||||
int ascii_key = 'a' + (ch - 1);
|
||||
CommandId id;
|
||||
if (KLookupCtrlCommand(ascii_key, id)) {
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Emacs-style movement aliases
|
||||
if (ch == CTRL('N')) { // C-n: down
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('P')) { // C-p: up
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('F')) { // C-f: right/forward
|
||||
out = {true, CommandId::MoveRight, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('B')) { // C-b: left/back
|
||||
out = {true, CommandId::MoveLeft, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('A')) {
|
||||
out = {true, CommandId::MoveHome, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ch == CTRL('E')) {
|
||||
out = {true, CommandId::MoveEnd, "", 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enter
|
||||
@@ -142,18 +127,15 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
||||
return true;
|
||||
}
|
||||
|
||||
// If previous key was ESC, interpret as meta
|
||||
// If previous key was ESC, interpret as meta and use ESC keymap
|
||||
if (esc_meta) {
|
||||
esc_meta = false;
|
||||
int ascii_key = ch;
|
||||
if (ascii_key >= 'A' && ascii_key <= 'Z')
|
||||
ascii_key = ascii_key - 'A' + 'a';
|
||||
if (ascii_key == 'b') {
|
||||
out = {true, CommandId::WordPrev, "", 0};
|
||||
return true;
|
||||
}
|
||||
if (ascii_key == 'f') {
|
||||
out = {true, CommandId::WordNext, "", 0};
|
||||
CommandId id;
|
||||
if (KLookupEscCommand(ascii_key, id)) {
|
||||
out = {true, id, "", 0};
|
||||
return true;
|
||||
}
|
||||
// Unhandled meta key: no command
|
||||
|
||||
@@ -154,7 +154,8 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
attron(A_REVERSE);
|
||||
|
||||
// Fill the status line with spaces first
|
||||
for (int i = 0; i < cols; ++i) addch(' ');
|
||||
for (int i = 0; i < cols; ++i)
|
||||
addch(' ');
|
||||
|
||||
// Build left segment
|
||||
std::string left;
|
||||
@@ -216,14 +217,17 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
}
|
||||
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;
|
||||
if (llen > left_max)
|
||||
llen = left_max;
|
||||
|
||||
// Draw left
|
||||
if (llen > 0) mvaddnstr(rows - 1, 0, left.c_str(), llen);
|
||||
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);
|
||||
if (rlen > 0)
|
||||
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
|
||||
|
||||
// Middle message
|
||||
const std::string &msg = ed.Status();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
class TerminalRenderer final : public Renderer {
|
||||
public:
|
||||
TerminalRenderer();
|
||||
|
||||
~TerminalRenderer() override;
|
||||
|
||||
void Draw(Editor &ed) override;
|
||||
|
||||
Reference in New Issue
Block a user