diff --git a/.idea/workspace.xml b/.idea/workspace.xml index e543175..e912af8 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -35,10 +35,15 @@ - + + + + + + diff --git a/Command.cc b/Command.cc index 55ea888..5afa93e 100644 --- a/Command.cc +++ b/Command.cc @@ -365,9 +365,23 @@ cmd_refresh(CommandContext &ctx) static bool cmd_kprefix(CommandContext &ctx) { - // Show k-command mode hint in status - ctx.editor.SetStatus("C-k _"); - return true; + // Show k-command mode hint in status + ctx.editor.SetStatus("C-k _"); + return true; +} + + +static bool +cmd_unknown_kcommand(CommandContext &ctx) +{ + char ch = '?'; + if (!ctx.arg.empty()) { + ch = ctx.arg[0]; + } + char buf[64]; + std::snprintf(buf, sizeof(buf), "unknown k-command %c", ch); + ctx.editor.SetStatus(buf); + return true; } @@ -887,57 +901,75 @@ cmd_move_end(CommandContext &ctx) static bool cmd_page_up(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) - return false; - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0; - if (content_rows == 0) - content_rows = 1; - while (repeat-- > 0) { - if (y > content_rows) - y -= content_rows; - else - y = 0; - if (x > rows[y].size()) - x = rows[y].size(); - } - buf->SetCursor(x, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + int repeat = ctx.count > 0 ? ctx.count : 1; + std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0; + if (content_rows == 0) + content_rows = 1; + + // Base on current top-of-screen (row offset) + std::size_t rowoffs = buf->Rowoffs(); + while (repeat-- > 0) { + if (rowoffs >= content_rows) + rowoffs -= content_rows; + else + rowoffs = 0; + } + // Clamp to valid range + if (rows.size() > content_rows) { + std::size_t max_top = rows.size() - content_rows; + if (rowoffs > max_top) rowoffs = max_top; + } else { + rowoffs = 0; + } + // Move cursor to first visible line, column 0 + std::size_t y = rowoffs; + if (y >= rows.size()) y = rows.empty() ? 0 : rows.size() - 1; + buf->SetOffsets(rowoffs, 0); + buf->SetCursor(0, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } static bool cmd_page_down(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) - return false; - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0; - if (content_rows == 0) - content_rows = 1; - while (repeat-- > 0) { - std::size_t max_down = rows.empty() ? 0 : (rows.size() - 1 - y); - if (content_rows < max_down) - y += content_rows; - else - y += max_down; - if (x > rows[y].size()) - x = rows[y].size(); - } - buf->SetCursor(x, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + int repeat = ctx.count > 0 ? ctx.count : 1; + std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0; + if (content_rows == 0) + content_rows = 1; + + std::size_t rowoffs = buf->Rowoffs(); + // Compute maximum top offset + std::size_t max_top = 0; + if (!rows.empty()) { + if (rows.size() > content_rows) + max_top = rows.size() - content_rows; + else + max_top = 0; + } + while (repeat-- > 0) { + if (rowoffs + content_rows <= max_top) + rowoffs += content_rows; + else + rowoffs = max_top; + } + // Move cursor to first visible line, column 0 + std::size_t y = std::min(rowoffs, rows.empty() ? 0 : rows.size() - 1); + buf->SetOffsets(rowoffs, 0); + buf->SetCursor(0, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } @@ -1115,11 +1147,13 @@ 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::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit}); - CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh}); - CommandRegistry::Register( - {CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix}); - CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start}); + 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( + {CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix}); + CommandRegistry::Register({CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)", + cmd_unknown_kcommand}); + CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start}); CommandRegistry::Register({ CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start }); diff --git a/Command.h b/Command.h index 65b4123..a9196fa 100644 --- a/Command.h +++ b/Command.h @@ -41,6 +41,8 @@ enum class CommandId { WordNext, // Direct cursor placement MoveCursorTo, // arg: "y:x" (zero-based row:col) + // Meta + UnknownKCommand, // arg: single character that was not recognized after C-k }; diff --git a/GUIFrontend.cc b/GUIFrontend.cc index 474a78c..4688cb7 100644 --- a/GUIFrontend.cc +++ b/GUIFrontend.cc @@ -19,6 +19,7 @@ static const char *kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatib bool GUIFrontend::Init(Editor &ed) { + (void)ed; // editor dimensions will be initialized during the first Step() frame if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { return false; } @@ -57,12 +58,11 @@ GUIFrontend::Init(Editor &ed) if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) return false; - // Initialize editor reported dimensions to pixels for now - int w, h; - SDL_GetWindowSize(window_, &w, &h); - width_ = w; - height_ = h; - ed.SetDimensions(static_cast(height_), static_cast(width_)); + // Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists + int w, h; + SDL_GetWindowSize(window_, &w, &h); + width_ = w; + height_ = h; // Initialize GUI font from embedded default LoadGuiFont_(nullptr, 16.f); @@ -81,14 +81,12 @@ GUIFrontend::Step(Editor &ed, bool &running) case SDL_QUIT: running = false; break; - case SDL_WINDOWEVENT: - if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { - width_ = e.window.data1; - height_ = e.window.data2; - ed.SetDimensions(static_cast(height_), - static_cast(width_)); - } - break; + case SDL_WINDOWEVENT: + if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + width_ = e.window.data1; + height_ = e.window.data2; + } + break; default: break; } @@ -96,7 +94,7 @@ GUIFrontend::Step(Editor &ed, bool &running) input_.ProcessSDLEvent(e); } - // Execute pending mapped inputs (drain queue) + // Execute pending mapped inputs (drain queue) for (;;) { MappedInput mi; if (!input_.Poll(mi)) @@ -109,10 +107,43 @@ GUIFrontend::Step(Editor &ed, bool &running) } } - // Start a new ImGui frame - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplSDL2_NewFrame(window_); - ImGui::NewFrame(); + // Start a new ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL2_NewFrame(window_); + ImGui::NewFrame(); + + // Update editor logical rows/cols using current ImGui metrics and display size + { + 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; + // Prefer ImGui IO display size; fall back to cached SDL window size + float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast(width_); + float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast(height_); + + // Account for the GUI window padding and the status bar height used in GUIRenderer + const ImGuiStyle &style = ImGui::GetStyle(); + float pad_x = style.WindowPadding.x; + float pad_y = style.WindowPadding.y; + // Status bar reserves one frame height (with spacing) inside the window + float status_h = ImGui::GetFrameHeightWithSpacing(); + + float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x); + float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h); + + // Visible content rows inside the scroll child + std::size_t content_rows = static_cast(std::floor(avail_h / line_h)); + // Editor::Rows includes the status line; add 1 back for it. + std::size_t rows = std::max(1, content_rows + 1); + std::size_t cols = static_cast(std::max(1.0f, std::floor(avail_w / ch_w))); + + // Only update if changed to avoid churn + if (rows != ed.Rows() || cols != ed.Cols()) { + ed.SetDimensions(rows, cols); + } + } // No runtime font UI; always use embedded font. diff --git a/GUIInputHandler.cc b/GUIInputHandler.cc index 30c1e91..85a266b 100644 --- a/GUIInputHandler.cc +++ b/GUIInputHandler.cc @@ -62,6 +62,18 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput k_prefix = true; out = {true, CommandId::KPrefix, "", 0}; return true; + case SDLK_n: // C-n: down + out = {true, CommandId::MoveDown, "", 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; @@ -103,25 +115,30 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput } } - if (k_prefix) { - k_prefix = false; - // Normalize SDL key to ASCII where possible - int ascii_key = 0; - if (key >= SDLK_SPACE && key <= SDLK_z) { - ascii_key = static_cast(key); - } - bool ctrl2 = (mod & KMOD_CTRL) != 0; - if (ascii_key != 0) { - ascii_key = KLowerAscii(ascii_key); - CommandId id; - if (KLookupKCommand(ascii_key, ctrl2, id)) { - out = {true, id, "", 0}; - return true; - } - } - out.hasCommand = false; - return true; - } + if (k_prefix) { + k_prefix = false; + // Normalize SDL key to ASCII where possible + int ascii_key = 0; + if (key >= SDLK_SPACE && key <= SDLK_z) { + ascii_key = static_cast(key); + } + bool ctrl2 = (mod & KMOD_CTRL) != 0; + if (ascii_key != 0) { + ascii_key = KLowerAscii(ascii_key); + CommandId id; + if (KLookupKCommand(ascii_key, ctrl2, id)) { + out = {true, id, "", 0}; + return true; + } + // Unknown k-command: report the typed character + char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast(ascii_key) : '?'; + std::string arg(1, c); + out = {true, CommandId::UnknownKCommand, arg, 0}; + return true; + } + out.hasCommand = false; + return true; + } return false; } diff --git a/GUIRenderer.cc b/GUIRenderer.cc index 42f3e19..d115b05 100644 --- a/GUIRenderer.cc +++ b/GUIRenderer.cc @@ -56,40 +56,77 @@ GUIRenderer::Draw(Editor &ed) const float line_h = ImGui::GetTextLineHeight(); const float row_h = ImGui::GetTextLineHeightWithSpacing(); const float space_w = ImGui::CalcTextSize(" ").x; - // When the user scrolls and the cursor is off-screen, move it to the nearest visible row - { - static float prev_scroll_y = -1.0f; - float child_h = ImGui::GetWindowHeight(); // child window height - long first_row = static_cast(scroll_y / row_h); - long vis_rows = static_cast(child_h / row_h); - if (vis_rows < 1) - vis_rows = 1; - long last_row = first_row + vis_rows - 1; - if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) { - long cyr = static_cast(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 >= static_cast(lines.size())) { - new_row = static_cast(lines.empty() ? 0 : (lines.size() - 1)); - } - // Clamp column to line length - std::size_t new_col = 0; - if (!lines.empty()) { - const std::string &l = lines[static_cast(new_row)]; - new_col = std::min(cx, l.size()); - } - char tmp2[64]; - std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col); - Execute(ed, CommandId::MoveCursorTo, std::string(tmp2)); - // refresh local variables after move - cy = buf->Cury(); - cx = buf->Curx(); - } - } - prev_scroll_y = scroll_y; - } + // If the command layer requested a specific top-of-screen (via Buffer::Rowoffs), + // force the ImGui scroll to match so paging aligns the first visible row. + bool forced_scroll = false; + { + std::size_t desired_top = buf->Rowoffs(); + long current_top = static_cast(scroll_y / row_h); + if (static_cast(desired_top) != current_top) { + ImGui::SetScrollY(static_cast(desired_top) * row_h); + scroll_y = ImGui::GetScrollY(); + forced_scroll = true; + } + } + // Synchronize cursor and scrolling. + // A) When the user scrolls and the cursor goes off-screen, move the cursor to the nearest visible row. + // B) When the cursor moves (via keyboard commands), scroll it back into view. + { + static float prev_scroll_y = -1.0f; + static long prev_cursor_y = -1; + // Compute visible row range using the child window height + float child_h = ImGui::GetWindowHeight(); + long first_row = static_cast(scroll_y / row_h); + long vis_rows = static_cast(child_h / row_h); + 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 + if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) { + long cyr = static_cast(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 >= static_cast(lines.size())) + new_row = static_cast(lines.empty() ? 0 : (lines.size() - 1)); + // Clamp column to line length + std::size_t new_col = 0; + if (!lines.empty()) { + const std::string &l = lines[static_cast(new_row)]; + new_col = std::min(cx, l.size()); + } + char tmp2[64]; + std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col); + Execute(ed, CommandId::MoveCursorTo, std::string(tmp2)); + cy = buf->Cury(); + cx = buf->Curx(); + cyr = static_cast(cy); + // Update visible range again in case content changed + first_row = static_cast(ImGui::GetScrollY() / row_h); + last_row = first_row + vis_rows - 1; + } + } + + // B) If cursor moved since last frame and is outside the visible region, scroll to reveal it + // Skip this when we just forced a top-of-screen alignment this frame. + if (!forced_scroll && prev_cursor_y >= 0 && static_cast(cy) != prev_cursor_y) { + long cyr = static_cast(cy); + if (cyr < first_row || cyr > last_row) { + float target = (static_cast(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; + ImGui::SetScrollY(target); + // refresh local variables + scroll_y = ImGui::GetScrollY(); + first_row = static_cast(scroll_y / row_h); + last_row = first_row + vis_rows - 1; + } + } + + prev_scroll_y = ImGui::GetScrollY(); + prev_cursor_y = static_cast(cy); + } // Handle mouse click before rendering to avoid dependent on drawn items if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { ImVec2 mp = ImGui::GetIO().MousePos; diff --git a/TerminalFrontend.cc b/TerminalFrontend.cc index 5b12f2b..e0630e0 100644 --- a/TerminalFrontend.cc +++ b/TerminalFrontend.cc @@ -9,15 +9,21 @@ bool TerminalFrontend::Init(Editor &ed) { - initscr(); - cbreak(); - noecho(); - keypad(stdscr, TRUE); - nodelay(stdscr, TRUE); - curs_set(1); - // Enable mouse support if available - mouseinterval(0); - mousemask(ALL_MOUSE_EVENTS, nullptr); + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + // Enable 8-bit meta key sequences (Alt/ESC-prefix handling in terminals) + meta(stdscr, TRUE); + // Make ESC key sequences resolve quickly so ESC+ works as meta +#ifdef set_escdelay + set_escdelay(50); +#endif + nodelay(stdscr, TRUE); + curs_set(1); + // Enable mouse support if available + mouseinterval(0); + mousemask(ALL_MOUSE_EVENTS, nullptr); int r = 0, c = 0; getmaxyx(stdscr, r, c); diff --git a/TerminalInputHandler.cc b/TerminalInputHandler.cc index 2ce38c3..74159cc 100644 --- a/TerminalInputHandler.cc +++ b/TerminalInputHandler.cc @@ -103,6 +103,23 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou out = {true, CommandId::FindStart, "", 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; @@ -144,26 +161,29 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou return true; } - if (k_prefix) { - k_prefix = false; // single next key only - // Determine if this is a control chord (e.g., C-x) and normalize - bool ctrl = false; - int ascii_key = ch; - if (ch >= 1 && ch <= 26) { - ctrl = true; - ascii_key = 'a' + (ch - 1); - } - // For letters, normalize to lowercase ASCII - ascii_key = KLowerAscii(ascii_key); + if (k_prefix) { + k_prefix = false; // single next key only + // Determine if this is a control chord (e.g., C-x) and normalize + bool ctrl = false; + int ascii_key = ch; + if (ch >= 1 && ch <= 26) { + ctrl = true; + ascii_key = 'a' + (ch - 1); + } + // For letters, normalize to lowercase ASCII + ascii_key = KLowerAscii(ascii_key); - CommandId id; - if (KLookupKCommand(ascii_key, ctrl, id)) { - out = {true, id, "", 0}; - } else { - out.hasCommand = false; // unknown chord after C-k - } - return true; - } + CommandId id; + if (KLookupKCommand(ascii_key, ctrl, id)) { + out = {true, id, "", 0}; + } else { + // Show unknown k-command message with the typed character + char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast(ascii_key) : '?'; + std::string arg(1, c); + out = {true, CommandId::UnknownKCommand, arg, 0}; + } + return true; + } // Printable ASCII if (ch >= 0x20 && ch <= 0x7E) { diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc index 3ec5467..14383eb 100644 --- a/TerminalRenderer.cc +++ b/TerminalRenderer.cc @@ -32,7 +32,8 @@ TerminalRenderer::Draw(Editor &ed) const Buffer *buf = ed.CurrentBuffer(); int content_rows = rows - 1; // last line is status - if (buf) { + 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(); @@ -138,12 +139,15 @@ TerminalRenderer::Draw(Editor &ed) std::size_t rx = buf->Rx(); // render x computed by command layer int cur_y = static_cast(cy) - static_cast(buf->Rowoffs()); int cur_x = static_cast(rx) - static_cast(buf->Coloffs()); - if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) { - move(cur_y, cur_x); - } - } else { - mvaddstr(0, 0, "[no buffer]"); - } + 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); @@ -237,5 +241,11 @@ TerminalRenderer::Draw(Editor &ed) 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(); } diff --git a/main.cc b/main.cc index 31b549c..855a1b7 100644 --- a/main.cc +++ b/main.cc @@ -33,8 +33,6 @@ main(int argc, const char *argv[]) { Editor editor; - std::cout << "v" << KTE_VERSION_STR << std::endl; - // CLI parsing using getopt_long bool req_gui = false; bool req_term = false;