2 Commits

Author SHA1 Message Date
483ff18b0d Add ScrollUp and ScrollDown commands for viewport scrolling, refine mouse wheel handling in GUI and terminal, and bump version to 1.2.2.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-02 02:53:02 -08:00
cd33e8feb1 Refactor scrolling logic for GUIRenderer and terminal to improve synchronization and cursor visibility. 2025-12-02 02:43:05 -08:00
9 changed files with 413 additions and 345 deletions

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "1.2.1") set(KTE_VERSION "1.2.2")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
@@ -16,36 +16,36 @@ option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF) option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
if (CMAKE_HOST_UNIX) if (CMAKE_HOST_UNIX)
message(STATUS "Build system is POSIX.") message(STATUS "Build system is POSIX.")
else () else ()
message(STATUS "Build system is NOT POSIX.") message(STATUS "Build system is NOT POSIX.")
endif () endif ()
if (MSVC) if (MSVC)
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>") add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
else () else ()
add_compile_options( add_compile_options(
"-Wall" "-Wall"
"-Wextra" "-Wextra"
"-Werror" "-Werror"
"$<$<CONFIG:DEBUG>:-g>" "$<$<CONFIG:DEBUG>:-g>"
"$<$<CONFIG:RELEASE>:-O2>") "$<$<CONFIG:RELEASE>:-O2>")
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options("-stdlib=libc++") add_compile_options("-stdlib=libc++")
else () else ()
# nothing special for gcc at the moment # nothing special for gcc at the moment
endif () endif ()
endif () endif ()
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME}) add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}") add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
add_compile_definitions(KTE_ENABLE_TREESITTER) add_compile_definitions(KTE_ENABLE_TREESITTER)
endif () endif ()
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}") message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
if (${BUILD_GUI}) if (${BUILD_GUI})
include(cmake/imgui.cmake) include(cmake/imgui.cmake)
endif () endif ()
# NCurses for terminal mode # NCurses for terminal mode
@@ -55,250 +55,250 @@ find_package(Curses REQUIRED)
include_directories(${CURSES_INCLUDE_DIR}) include_directories(${CURSES_INCLUDE_DIR})
set(SYNTAX_SOURCES set(SYNTAX_SOURCES
syntax/GoHighlighter.cc syntax/GoHighlighter.cc
syntax/CppHighlighter.cc syntax/CppHighlighter.cc
syntax/JsonHighlighter.cc syntax/JsonHighlighter.cc
syntax/ErlangHighlighter.cc syntax/ErlangHighlighter.cc
syntax/MarkdownHighlighter.cc syntax/MarkdownHighlighter.cc
syntax/TreeSitterHighlighter.cc syntax/TreeSitterHighlighter.cc
syntax/LispHighlighter.cc syntax/LispHighlighter.cc
syntax/HighlighterEngine.cc syntax/HighlighterEngine.cc
syntax/RustHighlighter.cc syntax/RustHighlighter.cc
syntax/HighlighterRegistry.cc syntax/HighlighterRegistry.cc
syntax/SqlHighlighter.cc syntax/SqlHighlighter.cc
syntax/NullHighlighter.cc syntax/NullHighlighter.cc
syntax/ForthHighlighter.cc syntax/ForthHighlighter.cc
syntax/PythonHighlighter.cc syntax/PythonHighlighter.cc
syntax/ShellHighlighter.cc syntax/ShellHighlighter.cc
) )
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
list(APPEND SYNTAX_SOURCES list(APPEND SYNTAX_SOURCES
TreeSitterHighlighter.cc) TreeSitterHighlighter.cc)
endif () endif ()
set(COMMON_SOURCES set(COMMON_SOURCES
GapBuffer.cc GapBuffer.cc
PieceTable.cc PieceTable.cc
Buffer.cc Buffer.cc
Editor.cc Editor.cc
Command.cc Command.cc
HelpText.cc HelpText.cc
KKeymap.cc KKeymap.cc
TerminalInputHandler.cc TerminalInputHandler.cc
TerminalRenderer.cc TerminalRenderer.cc
TerminalFrontend.cc TerminalFrontend.cc
TestInputHandler.cc TestInputHandler.cc
TestRenderer.cc TestRenderer.cc
TestFrontend.cc TestFrontend.cc
UndoNode.cc UndoNode.cc
UndoTree.cc UndoTree.cc
UndoSystem.cc UndoSystem.cc
${SYNTAX_SOURCES} ${SYNTAX_SOURCES}
) )
set(SYNTAX_HEADERS set(SYNTAX_HEADERS
syntax/GoHighlighter.h syntax/GoHighlighter.h
syntax/HighlighterEngine.h syntax/HighlighterEngine.h
syntax/ShellHighlighter.h syntax/ShellHighlighter.h
syntax/MarkdownHighlighter.h syntax/MarkdownHighlighter.h
syntax/LispHighlighter.h syntax/LispHighlighter.h
syntax/SqlHighlighter.h syntax/SqlHighlighter.h
syntax/ForthHighlighter.h syntax/ForthHighlighter.h
syntax/JsonHighlighter.h syntax/JsonHighlighter.h
syntax/TreeSitterHighlighter.h syntax/TreeSitterHighlighter.h
syntax/NullHighlighter.h syntax/NullHighlighter.h
syntax/CppHighlighter.h syntax/CppHighlighter.h
syntax/ErlangHighlighter.h syntax/ErlangHighlighter.h
syntax/LanguageHighlighter.h syntax/LanguageHighlighter.h
syntax/RustHighlighter.h syntax/RustHighlighter.h
syntax/PythonHighlighter.h syntax/PythonHighlighter.h
) )
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
list(APPEND THEME_HEADERS list(APPEND THEME_HEADERS
TreeSitterHighlighter.h) TreeSitterHighlighter.h)
endif () endif ()
set(THEME_HEADERS set(THEME_HEADERS
themes/ThemeHelpers.h themes/ThemeHelpers.h
themes/EInk.h themes/EInk.h
themes/Gruvbox.h themes/Gruvbox.h
themes/Solarized.h themes/Solarized.h
themes/Plan9.h themes/Plan9.h
themes/Nord.h themes/Nord.h
) )
set(COMMON_HEADERS set(COMMON_HEADERS
GapBuffer.h GapBuffer.h
PieceTable.h PieceTable.h
Buffer.h Buffer.h
Editor.h Editor.h
AppendBuffer.h AppendBuffer.h
Command.h Command.h
HelpText.h HelpText.h
KKeymap.h KKeymap.h
InputHandler.h InputHandler.h
TerminalInputHandler.h TerminalInputHandler.h
Renderer.h Renderer.h
TerminalRenderer.h TerminalRenderer.h
Frontend.h Frontend.h
TerminalFrontend.h TerminalFrontend.h
TestInputHandler.h TestInputHandler.h
TestRenderer.h TestRenderer.h
TestFrontend.h TestFrontend.h
UndoNode.h UndoNode.h
UndoTree.h UndoTree.h
UndoSystem.h UndoSystem.h
Highlight.h Highlight.h
${SYNTAX_HEADERS} ${SYNTAX_HEADERS}
${THEME_HEADERS} ${THEME_HEADERS}
) )
# kte (terminal-first) executable # kte (terminal-first) executable
add_executable(kte add_executable(kte
main.cc main.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
) )
if (KTE_USE_PIECE_TABLE) if (KTE_USE_PIECE_TABLE)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1) target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
endif () endif ()
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(kte ${CURSES_LIBRARIES}) target_link_libraries(kte ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
# Users can provide their own tree-sitter include/lib via cache variables # Users can provide their own tree-sitter include/lib via cache variables
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory") set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)") set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
if (TREESITTER_INCLUDE_DIR) if (TREESITTER_INCLUDE_DIR)
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR}) target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
endif () endif ()
if (TREESITTER_LIBRARY) if (TREESITTER_LIBRARY)
target_link_libraries(kte ${TREESITTER_LIBRARY}) target_link_libraries(kte ${TREESITTER_LIBRARY})
endif () endif ()
endif () endif ()
install(TARGETS kte install(TARGETS kte
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
# Man pages # Man pages
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
if (BUILD_TESTS) if (BUILD_TESTS)
# test_undo executable for testing undo/redo system # test_undo executable for testing undo/redo system
add_executable(test_undo add_executable(test_undo
test_undo.cc test_undo.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
) )
if (KTE_USE_PIECE_TABLE) if (KTE_USE_PIECE_TABLE)
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1) target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
endif () endif ()
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(test_undo ${CURSES_LIBRARIES}) target_link_libraries(test_undo ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER) if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR) if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR}) target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
endif () endif ()
if (TREESITTER_LIBRARY) if (TREESITTER_LIBRARY)
target_link_libraries(test_undo ${TREESITTER_LIBRARY}) target_link_libraries(test_undo ${TREESITTER_LIBRARY})
endif () endif ()
endif () endif ()
endif () endif ()
if (${BUILD_GUI}) if (${BUILD_GUI})
target_sources(kte PRIVATE target_sources(kte PRIVATE
Font.h Font.h
GUIConfig.cc GUIConfig.cc
GUIConfig.h GUIConfig.h
GUIRenderer.cc GUIRenderer.cc
GUIRenderer.h GUIRenderer.h
GUIInputHandler.cc GUIInputHandler.cc
GUIInputHandler.h GUIInputHandler.h
GUIFrontend.cc GUIFrontend.cc
GUIFrontend.h) GUIFrontend.h)
target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1) target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
target_link_libraries(kte imgui) target_link_libraries(kte imgui)
# kge (GUI-first) executable # kge (GUI-first) executable
add_executable(kge add_executable(kge
main.cc main.cc
${COMMON_SOURCES} ${COMMON_SOURCES}
${COMMON_HEADERS} ${COMMON_HEADERS}
GUIConfig.cc GUIConfig.cc
GUIConfig.h GUIConfig.h
GUIRenderer.cc GUIRenderer.cc
GUIRenderer.h GUIRenderer.h
GUIInputHandler.cc GUIInputHandler.cc
GUIInputHandler.h GUIInputHandler.h
GUIFrontend.cc GUIFrontend.cc
GUIFrontend.h) GUIFrontend.h)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE}) target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_UNDO_DEBUG) if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1) target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif () endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui) target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
# On macOS, build kge as a proper .app bundle # On macOS, build kge as a proper .app bundle
if (APPLE) if (APPLE)
# Define the icon file # Define the icon file
set(MACOSX_BUNDLE_ICON_FILE kge.icns) set(MACOSX_BUNDLE_ICON_FILE kge.icns)
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}") set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
# Add icon to the target sources and mark it as a resource # Add icon to the target sources and mark it as a resource
target_sources(kge PRIVATE ${kge_ICON}) target_sources(kge PRIVATE ${kge_ICON})
set_source_files_properties(${kge_ICON} PROPERTIES set_source_files_properties(${kge_ICON} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources) MACOSX_PACKAGE_LOCATION Resources)
# Configure Info.plist with version and identifiers # Configure Info.plist with version and identifiers
set(KGE_BUNDLE_ID "dev.wntrmute.kge") set(KGE_BUNDLE_ID "dev.wntrmute.kge")
configure_file( configure_file(
${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in ${CMAKE_CURRENT_LIST_DIR}/cmake/Info.plist.in
${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist ${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist
@ONLY) @ONLY)
set_target_properties(kge PROPERTIES set_target_properties(kge PROPERTIES
MACOSX_BUNDLE TRUE MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID} MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
MACOSX_BUNDLE_BUNDLE_NAME "kge" MACOSX_BUNDLE_BUNDLE_NAME "kge"
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE} MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist") MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
add_dependencies(kge kte) add_dependencies(kge kte)
add_custom_command(TARGET kge POST_BUILD add_custom_command(TARGET kge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:kte> $<TARGET_FILE:kte>
$<TARGET_FILE_DIR:kge>/kte $<TARGET_FILE_DIR:kge>/kte
COMMENT "Copying kte binary into kge.app bundle") COMMENT "Copying kte binary into kge.app bundle")
install(TARGETS kge install(TARGETS kge
BUNDLE DESTINATION . BUNDLE DESTINATION .
) )
install(TARGETS kte install(TARGETS kte
RUNTIME DESTINATION kge.app/Contents/MacOS RUNTIME DESTINATION kge.app/Contents/MacOS
) )
else () else ()
install(TARGETS kge install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
endif () endif ()
# Install kge man page only when GUI is built # Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons) install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
endif () endif ()

View File

@@ -42,12 +42,11 @@ compute_render_x(const std::string &line, const std::size_t curx, const std::siz
static void static void
ensure_cursor_visible(const Editor &ed, Buffer &buf) ensure_cursor_visible(const Editor &ed, Buffer &buf)
{ {
const std::size_t rows = ed.Rows();
const std::size_t cols = ed.Cols(); const std::size_t cols = ed.Cols();
if (rows == 0 || cols == 0) if (cols == 0)
return; return;
const std::size_t content_rows = rows > 0 ? rows - 1 : 0; // last row = status const std::size_t content_rows = ed.ContentRows();
const std::size_t cury = buf.Cury(); const std::size_t cury = buf.Cury();
const std::size_t curx = buf.Curx(); const std::size_t curx = buf.Curx();
std::size_t rowoffs = buf.Rowoffs(); std::size_t rowoffs = buf.Rowoffs();
@@ -3004,9 +3003,7 @@ cmd_page_up(CommandContext &ctx)
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0; std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
if (content_rows == 0)
content_rows = 1;
// Base on current top-of-screen (row offset) // Base on current top-of-screen (row offset)
std::size_t rowoffs = buf->Rowoffs(); std::size_t rowoffs = buf->Rowoffs();
@@ -3030,7 +3027,6 @@ cmd_page_up(CommandContext &ctx)
y = rows.empty() ? 0 : rows.size() - 1; y = rows.empty() ? 0 : rows.size() - 1;
buf->SetOffsets(rowoffs, 0); buf->SetOffsets(rowoffs, 0);
buf->SetCursor(0, y); buf->SetCursor(0, y);
ensure_cursor_visible(ctx.editor, *buf);
return true; return true;
} }
@@ -3046,9 +3042,7 @@ cmd_page_down(CommandContext &ctx)
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0; std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
if (content_rows == 0)
content_rows = 1;
std::size_t rowoffs = buf->Rowoffs(); std::size_t rowoffs = buf->Rowoffs();
// Compute maximum top offset // Compute maximum top offset
@@ -3069,7 +3063,74 @@ cmd_page_down(CommandContext &ctx)
std::size_t y = std::min<std::size_t>(rowoffs, rows.empty() ? 0 : rows.size() - 1); std::size_t y = std::min<std::size_t>(rowoffs, rows.empty() ? 0 : rows.size() - 1);
buf->SetOffsets(rowoffs, 0); buf->SetOffsets(rowoffs, 0);
buf->SetCursor(0, y); buf->SetCursor(0, y);
ensure_cursor_visible(ctx.editor, *buf); return true;
}
static bool
cmd_scroll_up(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf)
return false;
ensure_at_least_one_line(*buf);
const auto &rows = buf->Rows();
std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
std::size_t rowoffs = buf->Rowoffs();
// Scroll up by 3 lines (or count if specified), without moving cursor
int scroll_amount = ctx.count > 0 ? ctx.count : 3;
if (rowoffs >= static_cast<std::size_t>(scroll_amount))
rowoffs -= static_cast<std::size_t>(scroll_amount);
else
rowoffs = 0;
buf->SetOffsets(rowoffs, buf->Coloffs());
// If cursor is now below the visible area, move it to the last visible line
std::size_t cury = buf->Cury();
if (cury >= rowoffs + content_rows) {
std::size_t new_y = rowoffs + content_rows - 1;
if (new_y >= rows.size() && !rows.empty())
new_y = rows.size() - 1;
buf->SetCursor(buf->Curx(), new_y);
}
return true;
}
static bool
cmd_scroll_down(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf)
return false;
ensure_at_least_one_line(*buf);
const auto &rows = buf->Rows();
std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
std::size_t rowoffs = buf->Rowoffs();
// Scroll down by 3 lines (or count if specified), without moving cursor
int scroll_amount = ctx.count > 0 ? ctx.count : 3;
// Compute maximum top offset
std::size_t max_top = 0;
if (!rows.empty() && rows.size() > content_rows)
max_top = rows.size() - content_rows;
rowoffs += static_cast<std::size_t>(scroll_amount);
if (rowoffs > max_top)
rowoffs = max_top;
buf->SetOffsets(rowoffs, buf->Coloffs());
// If cursor is now above the visible area, move it to the first visible line
std::size_t cury = buf->Cury();
if (cury < rowoffs) {
buf->SetCursor(buf->Curx(), rowoffs);
}
return true; return true;
} }
@@ -3682,6 +3743,8 @@ InstallDefaultCommands()
CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end}); CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end});
CommandRegistry::Register({CommandId::PageUp, "page-up", "Page up", cmd_page_up}); CommandRegistry::Register({CommandId::PageUp, "page-up", "Page up", cmd_page_up});
CommandRegistry::Register({CommandId::PageDown, "page-down", "Page down", cmd_page_down}); CommandRegistry::Register({CommandId::PageDown, "page-down", "Page down", cmd_page_down});
CommandRegistry::Register({CommandId::ScrollUp, "scroll-up", "Scroll viewport up", cmd_scroll_up});
CommandRegistry::Register({CommandId::ScrollDown, "scroll-down", "Scroll viewport down", cmd_scroll_down});
CommandRegistry::Register({CommandId::WordPrev, "word-prev", "Move to previous word", cmd_word_prev}); CommandRegistry::Register({CommandId::WordPrev, "word-prev", "Move to previous word", cmd_word_prev});
CommandRegistry::Register({CommandId::WordNext, "word-next", "Move to next word", cmd_word_next}); CommandRegistry::Register({CommandId::WordNext, "word-next", "Move to next word", cmd_word_next});
CommandRegistry::Register({ CommandRegistry::Register({
@@ -3800,4 +3863,4 @@ Execute(Editor &ed, const std::string &name, const std::string &arg, int count)
return false; return false;
CommandContext ctx{ed, arg, count}; CommandContext ctx{ed, arg, count};
return cmd->handler ? cmd->handler(ctx) : false; return cmd->handler ? cmd->handler(ctx) : false;
} }

View File

@@ -58,6 +58,8 @@ enum class CommandId {
MoveEnd, MoveEnd,
PageUp, PageUp,
PageDown, PageDown,
ScrollUp, // scroll viewport up (towards beginning) without moving cursor
ScrollDown, // scroll viewport down (towards end) without moving cursor
WordPrev, WordPrev,
WordNext, WordNext,
DeleteWordPrev, // delete previous word (ESC BACKSPACE) DeleteWordPrev, // delete previous word (ESC BACKSPACE)

View File

@@ -32,6 +32,16 @@ public:
} }
[[nodiscard]] std::size_t ContentRows() const
{
// Always compute from current rows_ to avoid stale values.
// Reserve 1 row for status line.
if (rows_ == 0)
return 1;
return std::max<std::size_t>(1, rows_ - 1);
}
// Mode and flags (mirroring legacy fields) // Mode and flags (mirroring legacy fields)
void SetMode(int m) void SetMode(int m)
{ {
@@ -553,4 +563,4 @@ private:
std::string replace_with_tmp_; std::string replace_with_tmp_;
}; };
#endif // KTE_EDITOR_H #endif // KTE_EDITOR_H

View File

@@ -203,28 +203,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
input_.ProcessSDLEvent(e); input_.ProcessSDLEvent(e);
} }
// Execute pending mapped inputs (drain queue) // Start a new ImGui frame BEFORE processing commands so dimensions are correct
for (;;) {
MappedInput mi;
if (!input_.Poll(mi))
break;
if (mi.hasCommand) {
// Track kill ring before and after to sync GUI clipboard when it changes
const std::string before = ed.KillRingHead();
Execute(ed, mi.id, mi.arg, mi.count);
const std::string after = ed.KillRingHead();
if (after != before && !after.empty()) {
// Update the system clipboard to mirror the kill ring head in GUI
SDL_SetClipboardText(after.c_str());
}
}
}
if (ed.QuitRequested()) {
running = false;
}
// Start a new ImGui frame
ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame(window_); ImGui_ImplSDL2_NewFrame(window_);
ImGui::NewFrame(); ImGui::NewFrame();
@@ -264,6 +243,27 @@ GUIFrontend::Step(Editor &ed, bool &running)
} }
} }
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
for (;;) {
MappedInput mi;
if (!input_.Poll(mi))
break;
if (mi.hasCommand) {
// Track kill ring before and after to sync GUI clipboard when it changes
const std::string before = ed.KillRingHead();
Execute(ed, mi.id, mi.arg, mi.count);
const std::string after = ed.KillRingHead();
if (after != before && !after.empty()) {
// Update the system clipboard to mirror the kill ring head in GUI
SDL_SetClipboardText(after.c_str());
}
}
}
if (ed.QuitRequested()) {
running = false;
}
// No runtime font UI; always use embedded font. // No runtime font UI; always use embedded font.
// Draw editor UI // Draw editor UI
@@ -318,4 +318,4 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
} }
// No runtime font reload or system font resolution in this simplified build. // No runtime font reload or system font resolution in this simplified build.

View File

@@ -285,15 +285,11 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
bool produced = false; bool produced = false;
switch (e.type) { switch (e.type) {
case SDL_MOUSEWHEEL: { case SDL_MOUSEWHEEL: {
// If ImGui wants to capture the mouse (e.g., hovering the File Picker list), // Map vertical wheel to viewport scrolling (ScrollUp/ScrollDown)
// don't translate wheel events into editor scrolling. // Note: We don't check WantCaptureMouse here because ImGui sets it to true
// This prevents background buffer scroll while using GUI widgets. // whenever the mouse is over any ImGui window (including our editor content area).
ImGuiIO &io = ImGui::GetIO(); // The NoScrollWithMouse flag on the child window prevents ImGui from handling
if (io.WantCaptureMouse) { // scroll internally, so we can safely process wheel events ourselves.
return true; // consumed by GUI
}
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
int dy = e.wheel.y; int dy = e.wheel.y;
#ifdef SDL_MOUSEWHEEL_FLIPPED #ifdef SDL_MOUSEWHEEL_FLIPPED
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
@@ -301,7 +297,7 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
#endif #endif
if (dy != 0) { if (dy != 0) {
int repeat = dy > 0 ? dy : -dy; int repeat = dy > 0 ? dy : -dy;
CommandId id = dy > 0 ? CommandId::MoveUp : CommandId::MoveDown; CommandId id = dy > 0 ? CommandId::ScrollUp : CommandId::ScrollDown;
std::lock_guard<std::mutex> lk(mu_); std::lock_guard<std::mutex> lk(mu_);
for (int i = 0; i < repeat; ++i) { for (int i = 0; i < repeat; ++i) {
q_.push(MappedInput{true, id, std::string(), 0}); q_.push(MappedInput{true, id, std::string(), 0});
@@ -372,7 +368,7 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
// Digits without shift, or a plain '-' // Digits without shift, or a plain '-'
const bool is_digit_key = (key >= SDLK_0 && key <= SDLK_9) && !(mods & KMOD_SHIFT); const bool is_digit_key = (key >= SDLK_0 && key <= SDLK_9) && !(mods & KMOD_SHIFT);
const bool is_minus_key = (key == SDLK_MINUS); const bool is_minus_key = (key == SDLK_MINUS);
if (uarg_active_ && uarg_collecting_ && (is_digit_key || is_minus_key)) { if (uarg_active_ && uarg_collecting_ &&(is_digit_key || is_minus_key)) {
suppress_text_input_once_ = true; suppress_text_input_once_ = true;
} }
} }
@@ -564,7 +560,12 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
if (produced && mi.hasCommand) { if (produced && mi.hasCommand) {
// Attach universal-argument count if present, then clear the state // Attach universal-argument count if present, then clear the state
if (uarg_active_ && mi.id != CommandId::UArgStatus) { if (uarg_active_ &&mi
.
id != CommandId::UArgStatus
)
{
int count = 0; int count = 0;
if (!uarg_had_digits_ && !uarg_negative_) { if (!uarg_had_digits_ && !uarg_negative_) {
count = (uarg_value_ > 0) ? uarg_value_ : 4; count = (uarg_value_ > 0) ? uarg_value_ : 4;
@@ -597,4 +598,4 @@ GUIInputHandler::Poll(MappedInput &out)
out = q_.front(); out = q_.front();
q_.pop(); q_.pop();
return true; return true;
} }

View File

@@ -66,55 +66,66 @@ GUIRenderer::Draw(Editor &ed)
if (!buf) { if (!buf) {
ImGui::TextUnformatted("[no buffer]"); ImGui::TextUnformatted("[no buffer]");
} else { } else {
const auto &lines = buf->Rows(); const auto &lines = buf->Rows();
// Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Detect click-to-move inside this scroll region
ImVec2 list_origin = ImGui::GetCursorScreenPos();
float scroll_y = ImGui::GetScrollY();
float scroll_x = ImGui::GetScrollX();
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
std::size_t cy = buf->Cury(); std::size_t cy = buf->Cury();
std::size_t cx = buf->Curx(); std::size_t cx = buf->Curx();
const float line_h = ImGui::GetTextLineHeight(); const float line_h = ImGui::GetTextLineHeight();
const float row_h = ImGui::GetTextLineHeightWithSpacing(); const float row_h = ImGui::GetTextLineHeightWithSpacing();
const float space_w = ImGui::CalcTextSize(" ").x; const float space_w = ImGui::CalcTextSize(" ").x;
// Two-way sync between Buffer::Rowoffs and ImGui scroll position: // Two-way sync between Buffer::Rowoffs and ImGui scroll position:
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it. // - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view. // - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
// This prevents clicks/wheel from being immediately overridden by stale offsets. static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
const long buf_coloffs = static_cast<long>(buf->Coloffs());
// Detect programmatic change (e.g., page_down command changed rowoffs)
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
float target_y = static_cast<float>(buf_rowoffs) * row_h;
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
}
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
float target_x = static_cast<float>(buf_coloffs) * space_w;
float target_y = static_cast<float>(buf_rowoffs) * row_h;
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
}
// Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Get child window position and scroll for click handling
ImVec2 child_window_pos = ImGui::GetWindowPos();
float scroll_y = ImGui::GetScrollY();
float scroll_x = ImGui::GetScrollX();
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
bool forced_scroll = false; bool forced_scroll = false;
{ {
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
const long buf_coloffs = static_cast<long>(buf->Coloffs());
const long scroll_top = static_cast<long>(scroll_y / row_h); const long scroll_top = static_cast<long>(scroll_y / row_h);
const long scroll_left = static_cast<long>(scroll_x / space_w); const long scroll_left = static_cast<long>(scroll_x / space_w);
// Detect programmatic change (e.g., keyboard navigation ensured visibility) // Check if rowoffs was programmatically changed this frame
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) { if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
ImGui::SetScrollY(static_cast<float>(buf_rowoffs) * row_h);
scroll_y = ImGui::GetScrollY();
forced_scroll = true; forced_scroll = true;
} }
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
ImGui::SetScrollX(static_cast<float>(buf_coloffs) * space_w); // If user scrolled (not programmatic), update buffer offsets accordingly
scroll_x = ImGui::GetScrollX(); if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) {
forced_scroll = true;
}
// If user scrolled, update buffer offsets accordingly
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) { if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)), mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
mbuf->Coloffs()); mbuf->Coloffs());
} }
} }
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) { if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) { if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(mbuf->Rowoffs(), mbuf->SetOffsets(mbuf->Rowoffs(),
static_cast<std::size_t>(std::max(0L, scroll_left))); static_cast<std::size_t>(std::max(0L, scroll_left)));
@@ -122,11 +133,12 @@ GUIRenderer::Draw(Editor &ed)
} }
// Update trackers for next frame // Update trackers for next frame
prev_buf_rowoffs = static_cast<long>(buf->Rowoffs()); prev_scroll_y = scroll_y;
prev_buf_coloffs = static_cast<long>(buf->Coloffs()); prev_scroll_x = scroll_x;
prev_scroll_y = ImGui::GetScrollY();
prev_scroll_x = ImGui::GetScrollX();
} }
prev_buf_rowoffs = buf_rowoffs;
prev_buf_coloffs = buf_coloffs;
// Synchronize cursor and scrolling. // Synchronize cursor and scrolling.
// Ensure the cursor is visible even on the first frame or when it didn't move, // Ensure the cursor is visible even on the first frame or when it didn't move,
// unless we already forced scrolling from Buffer::Rowoffs this frame. // unless we already forced scrolling from Buffer::Rowoffs this frame.
@@ -165,24 +177,16 @@ GUIRenderer::Draw(Editor &ed)
// Handle mouse click before rendering to avoid dependent on drawn items // Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos; ImVec2 mp = ImGui::GetIO().MousePos;
// Compute viewport-relative row so (0) is top row of the visible area // Compute content-relative position accounting for scroll
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h; // mp.y - child_window_pos.y gives us pixels from top of child window
long vy = static_cast<long>(vy_f); // Adding scroll_y gives us pixels from top of content (buffer row 0)
if (vy < 0) float content_y = (mp.y - child_window_pos.y) + scroll_y;
vy = 0; long by_l = static_cast<long>(content_y / row_h);
if (by_l < 0)
by_l = 0;
// Clamp vy within visible content height to avoid huge jumps // Convert to buffer row
ImVec2 cr_min = ImGui::GetWindowContentRegionMin(); std::size_t by = static_cast<std::size_t>(by_l);
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
float child_h = (cr_max.y - cr_min.y);
long vis_rows = static_cast<long>(child_h / row_h);
if (vis_rows < 1)
vis_rows = 1;
if (vy >= vis_rows)
vy = vis_rows - 1;
// Translate viewport row to buffer row using Buffer::Rowoffs
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
if (by >= lines.size()) { if (by >= lines.size()) {
if (!lines.empty()) if (!lines.empty())
by = lines.size() - 1; by = lines.size() - 1;
@@ -190,58 +194,43 @@ GUIRenderer::Draw(Editor &ed)
by = 0; by = 0;
} }
// Compute desired pixel X inside the viewport content (subtract horizontal scroll) // Compute content-relative X position accounting for scroll
float px = (mp.x - list_origin.x - scroll_x); // mp.x - child_window_pos.x gives us pixels from left edge of child window
if (px < 0.0f) // Adding scroll_x gives us pixels from left edge of content (column 0)
px = 0.0f; float content_x = (mp.x - child_window_pos.x) + scroll_x;
if (content_x < 0.0f)
content_x = 0.0f;
// Empty buffer guard: if there are no lines yet, just move to 0:0 // Empty buffer guard: if there are no lines yet, just move to 0:0
if (lines.empty()) { if (lines.empty()) {
Execute(ed, CommandId::MoveCursorTo, std::string("0:0")); Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
} else { } else {
// Convert pixel X to a render-column target including horizontal col offset // Convert pixel X to source column accounting for tabs
// Use our own tab expansion of width 8 to match command layer logic.
std::string line_clicked = static_cast<std::string>(lines[by]); std::string line_clicked = static_cast<std::string>(lines[by]);
const std::size_t tabw = 8; const std::size_t tabw = 8;
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
// then translate to viewport-space by subtracting Coloffs.
std::size_t coloffs = buf->Coloffs();
std::size_t rx_abs = 0; // absolute rendered column
std::size_t i = 0; // source column iterator
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column // Iterate through source columns, computing rendered position, to find closest match
if (!line_clicked.empty() && coloffs > 0) { std::size_t rx = 0; // rendered column position
while (i < line_clicked.size() && rx_abs < coloffs) { std::size_t best_col = 0;
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
}
// Now search for closest source column to clicked px within/after viewport
std::size_t best_col = i; // default to first visible column
float best_dist = std::numeric_limits<float>::infinity(); float best_dist = std::numeric_limits<float>::infinity();
while (true) {
// For i in [current..size], evaluate candidate including the implicit end position for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0; // Check current position
float rx_px = static_cast<float>(rx_view) * space_w; float rx_px = static_cast<float>(rx) * space_w;
float dist = std::fabs(px - rx_px); float dist = std::fabs(content_x - rx_px);
if (dist <= best_dist) { if (dist < best_dist) {
best_dist = dist; best_dist = dist;
best_col = i; best_col = i;
} }
if (i == line_clicked.size())
break; // Advance to next position if not at end
// advance to next source column if (i < line_clicked.size()) {
if (line_clicked[i] == '\t') { if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw)); rx += (tabw - (rx % tabw));
} else { } else {
rx_abs += 1; rx += 1;
}
} }
++i;
} }
// Dispatch absolute buffer coordinates (row:col) // Dispatch absolute buffer coordinates (row:col)
@@ -374,7 +363,8 @@ GUIRenderer::Draw(Editor &ed)
p, col, expanded.c_str() + vx0, expanded.c_str() + vx1); p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
} }
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line. // We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + line_h)); // Use row_h (with spacing) to match click calculation and ensure consistent line positions.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
} else { } else {
// No syntax: draw as one run // No syntax: draw as one run
ImGui::TextUnformatted(expanded.c_str()); ImGui::TextUnformatted(expanded.c_str());

View File

@@ -35,16 +35,16 @@ map_key_to_command(const int ch,
case KEY_MOUSE: { case KEY_MOUSE: {
MEVENT ev{}; MEVENT ev{};
if (getmouse(&ev) == OK) { if (getmouse(&ev) == OK) {
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch // Mouse wheel → scroll viewport without moving cursor
#ifdef BUTTON4_PRESSED #ifdef BUTTON4_PRESSED
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) { if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
out = {true, CommandId::MoveUp, "", 0}; out = {true, CommandId::ScrollUp, "", 0};
return true; return true;
} }
#endif #endif
#ifdef BUTTON5_PRESSED #ifdef BUTTON5_PRESSED
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) { if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
out = {true, CommandId::MoveDown, "", 0}; out = {true, CommandId::ScrollDown, "", 0};
return true; return true;
} }
#endif #endif
@@ -320,4 +320,4 @@ TerminalInputHandler::Poll(MappedInput &out)
{ {
out = {}; out = {};
return decode_(out) && out.hasCommand; return decode_(out) && out.hasCommand;
} }

View File

@@ -34,6 +34,8 @@ TerminalRenderer::Draw(Editor &ed)
const Buffer *buf = ed.CurrentBuffer(); const Buffer *buf = ed.CurrentBuffer();
int content_rows = rows - 1; // last line is status 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 int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
if (buf) { if (buf) {