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">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
|
<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$/.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.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>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -140,7 +156,7 @@
|
|||||||
<option name="number" value="Default" />
|
<option name="number" value="Default" />
|
||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1764457173148</updated>
|
<updated>1764457173148</updated>
|
||||||
<workItem from="1764457174208" duration="20181000" />
|
<workItem from="1764457174208" duration="22207000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</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
|
These guidelines summarize the goals, interfaces, key operations, and current
|
||||||
development practices for kte.
|
development practices for kte.
|
||||||
|
|
||||||
|
Style note: all code should be formatted with the current CLion C++ style.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
- Keep the core small, fast, and understandable.
|
- Keep the core small, fast, and understandable.
|
||||||
@@ -28,10 +30,10 @@ Prerequisites: a C++17 compiler, CMake, and ncurses development headers/libs.
|
|||||||
- Debian/Ubuntu: `sudo apt-get install libncurses5-dev libncursesw5-dev`
|
- Debian/Ubuntu: `sudo apt-get install libncurses5-dev libncursesw5-dev`
|
||||||
|
|
||||||
- Configure and build (example):
|
- Configure and build (example):
|
||||||
- `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug`
|
- `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug`
|
||||||
- `cmake --build cmake-build-debug`
|
- `cmake --build cmake-build-debug`
|
||||||
- Run:
|
- Run:
|
||||||
- `./cmake-build-debug/kte [files]`
|
- `./cmake-build-debug/kte [files]`
|
||||||
|
|
||||||
Project entry point: `main.cpp`
|
Project entry point: `main.cpp`
|
||||||
|
|
||||||
|
|||||||
69
Command.cc
69
Command.cc
@@ -296,7 +296,22 @@ cmd_save_as(CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_quit(CommandContext &ctx)
|
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");
|
ctx.editor.SetStatus("Quit requested");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -329,6 +344,16 @@ cmd_save_and_quit(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.editor.SetStatus("Save and quit requested");
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1065,24 +1090,7 @@ cmd_word_next(CommandContext &ctx)
|
|||||||
while (repeat-- > 0) {
|
while (repeat-- > 0) {
|
||||||
if (y >= rows.size())
|
if (y >= rows.size())
|
||||||
break;
|
break;
|
||||||
// Skip whitespace to the right
|
// First, if currently on a word, skip to its end
|
||||||
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
|
|
||||||
while (y < rows.size()) {
|
while (y < rows.size()) {
|
||||||
if (x < rows[y].size() && is_word_char(static_cast<unsigned char>(rows[y][x]))) {
|
if (x < rows[y].size() && is_word_char(static_cast<unsigned char>(rows[y][x]))) {
|
||||||
++x;
|
++x;
|
||||||
@@ -1097,6 +1105,23 @@ cmd_word_next(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
break;
|
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);
|
buf->SetCursor(x, y);
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
@@ -1163,6 +1188,7 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register({CommandId::Save, "save", "Save current buffer", cmd_save});
|
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::SaveAs, "save-as", "Save current buffer as...", cmd_save_as});
|
||||||
CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit});
|
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::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
|
||||||
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
|
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
|
||||||
CommandRegistry::Register(
|
CommandRegistry::Register(
|
||||||
@@ -1205,6 +1231,11 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count)
|
|||||||
const Command *cmd = CommandRegistry::FindById(id);
|
const Command *cmd = CommandRegistry::FindById(id);
|
||||||
if (!cmd)
|
if (!cmd)
|
||||||
return false;
|
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};
|
CommandContext ctx{ed, arg, count};
|
||||||
return cmd->handler ? cmd->handler(ctx) : false;
|
return cmd->handler ? cmd->handler(ctx) : false;
|
||||||
}
|
}
|
||||||
|
|||||||
88
Command.h
88
Command.h
@@ -14,47 +14,48 @@ class Editor;
|
|||||||
// Identifiers for editor commands. This is intentionally small for now and
|
// Identifiers for editor commands. This is intentionally small for now and
|
||||||
// will grow as features are implemented.
|
// will grow as features are implemented.
|
||||||
enum class CommandId {
|
enum class CommandId {
|
||||||
Save,
|
Save,
|
||||||
SaveAs,
|
SaveAs,
|
||||||
// Placeholders for future commands from ke's model
|
// Placeholders for future commands from ke's model
|
||||||
Quit,
|
Quit,
|
||||||
SaveAndQuit,
|
QuitNow, // immediate quit, no confirmation
|
||||||
Refresh, // force redraw
|
SaveAndQuit,
|
||||||
KPrefix, // show "C-k _" prompt in status when entering k-command
|
Refresh, // force redraw
|
||||||
FindStart, // begin incremental search (placeholder)
|
KPrefix, // show "C-k _" prompt in status when entering k-command
|
||||||
OpenFileStart, // begin open-file prompt
|
FindStart, // begin incremental search (placeholder)
|
||||||
// Editing
|
OpenFileStart, // begin open-file prompt
|
||||||
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
// Editing
|
||||||
Newline, // insert a newline at cursor
|
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
||||||
Backspace, // delete char before cursor (may join lines)
|
Newline, // insert a newline at cursor
|
||||||
DeleteChar, // delete char at cursor (may join lines)
|
Backspace, // delete char before cursor (may join lines)
|
||||||
// Navigation (basic)
|
DeleteChar, // delete char at cursor (may join lines)
|
||||||
MoveLeft,
|
// Navigation (basic)
|
||||||
MoveRight,
|
MoveLeft,
|
||||||
MoveUp,
|
MoveRight,
|
||||||
MoveDown,
|
MoveUp,
|
||||||
MoveHome,
|
MoveDown,
|
||||||
MoveEnd,
|
MoveHome,
|
||||||
PageUp,
|
MoveEnd,
|
||||||
PageDown,
|
PageUp,
|
||||||
WordPrev,
|
PageDown,
|
||||||
WordNext,
|
WordPrev,
|
||||||
// Direct cursor placement
|
WordNext,
|
||||||
MoveCursorTo, // arg: "y:x" (zero-based row:col)
|
// Direct cursor placement
|
||||||
// Meta
|
MoveCursorTo, // arg: "y:x" (zero-based row:col)
|
||||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
// Meta
|
||||||
|
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Context passed to command handlers.
|
// Context passed to command handlers.
|
||||||
struct CommandContext {
|
struct CommandContext {
|
||||||
Editor &editor;
|
Editor &editor;
|
||||||
|
|
||||||
// Optional argument string (e.g., filename for SaveAs).
|
// Optional argument string (e.g., filename for SaveAs).
|
||||||
std::string arg;
|
std::string arg;
|
||||||
|
|
||||||
// Optional repeat count (C-u support). 0 means not provided.
|
// Optional repeat count (C-u support). 0 means not provided.
|
||||||
int count = 0;
|
int count = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -62,26 +63,26 @@ using CommandHandler = std::function<bool(CommandContext &)>; // return true on
|
|||||||
|
|
||||||
|
|
||||||
struct Command {
|
struct Command {
|
||||||
CommandId id;
|
CommandId id;
|
||||||
std::string name; // stable, unique name (e.g., "save", "save-as")
|
std::string name; // stable, unique name (e.g., "save", "save-as")
|
||||||
std::string help; // short help/description
|
std::string help; // short help/description
|
||||||
CommandHandler handler;
|
CommandHandler handler;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Simple global registry. Not thread-safe; suitable for this app.
|
// Simple global registry. Not thread-safe; suitable for this app.
|
||||||
class CommandRegistry {
|
class CommandRegistry {
|
||||||
public:
|
public:
|
||||||
static void Register(const Command &cmd);
|
static void Register(const Command &cmd);
|
||||||
|
|
||||||
static const Command *FindById(CommandId id);
|
static const Command *FindById(CommandId id);
|
||||||
|
|
||||||
static const Command *FindByName(const std::string &name);
|
static const Command *FindByName(const std::string &name);
|
||||||
|
|
||||||
static const std::vector<Command> &All();
|
static const std::vector<Command> &All();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static std::vector<Command> &storage_();
|
static std::vector<Command> &storage_();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ void InstallDefaultCommands();
|
|||||||
// Dispatcher entry points for the input layer
|
// Dispatcher entry points for the input layer
|
||||||
// Returns true if the command executed successfully.
|
// 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, 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);
|
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||||
|
|
||||||
#endif // KTE_COMMAND_H
|
#endif // KTE_COMMAND_H
|
||||||
|
|||||||
@@ -113,9 +113,11 @@ Editor::Reset()
|
|||||||
no_kill_ = 0;
|
no_kill_ = 0;
|
||||||
dirtyex_ = 0;
|
dirtyex_ = 0;
|
||||||
msg_.clear();
|
msg_.clear();
|
||||||
msgtm_ = 0;
|
msgtm_ = 0;
|
||||||
uarg_ = 0;
|
uarg_ = 0;
|
||||||
ucount_ = 0;
|
ucount_ = 0;
|
||||||
|
quit_requested_ = false;
|
||||||
|
quit_confirm_pending_ = false;
|
||||||
buffers_.clear();
|
buffers_.clear();
|
||||||
curbuf_ = 0;
|
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
|
[[nodiscard]] std::time_t StatusTime() const
|
||||||
{
|
{
|
||||||
return msgtm_;
|
return msgtm_;
|
||||||
@@ -357,6 +382,10 @@ private:
|
|||||||
std::vector<Buffer> buffers_;
|
std::vector<Buffer> buffers_;
|
||||||
std::size_t curbuf_ = 0; // index into buffers_
|
std::size_t curbuf_ = 0; // index into buffers_
|
||||||
|
|
||||||
|
// Quit state
|
||||||
|
bool quit_requested_ = false;
|
||||||
|
bool quit_confirm_pending_ = false;
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
bool search_active_ = false;
|
bool search_active_ = false;
|
||||||
std::string search_query_;
|
std::string search_query_;
|
||||||
|
|||||||
154
GUIFrontend.cc
154
GUIFrontend.cc
@@ -19,7 +19,7 @@ static const char *kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatib
|
|||||||
bool
|
bool
|
||||||
GUIFrontend::Init(Editor &ed)
|
GUIFrontend::Init(Editor &ed)
|
||||||
{
|
{
|
||||||
(void)ed; // editor dimensions will be initialized during the first Step() frame
|
(void) ed; // editor dimensions will be initialized during the first Step() frame
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -49,25 +49,25 @@ GUIFrontend::Init(Editor &ed)
|
|||||||
|
|
||||||
IMGUI_CHECKVERSION();
|
IMGUI_CHECKVERSION();
|
||||||
ImGui::CreateContext();
|
ImGui::CreateContext();
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
(void) io;
|
(void) io;
|
||||||
ImGui::StyleColorsDark();
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||||
return false;
|
return false;
|
||||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists
|
// Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists
|
||||||
int w, h;
|
int w, h;
|
||||||
SDL_GetWindowSize(window_, &w, &h);
|
SDL_GetWindowSize(window_, &w, &h);
|
||||||
width_ = w;
|
width_ = w;
|
||||||
height_ = h;
|
height_ = h;
|
||||||
|
|
||||||
// Initialize GUI font from embedded default
|
// Initialize GUI font from embedded default
|
||||||
LoadGuiFont_(nullptr, 16.f);
|
LoadGuiFont_(nullptr, 16.f);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -78,77 +78,80 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
while (SDL_PollEvent(&e)) {
|
while (SDL_PollEvent(&e)) {
|
||||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case SDL_QUIT:
|
case SDL_QUIT:
|
||||||
running = false;
|
running = false;
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||||
width_ = e.window.data1;
|
width_ = e.window.data1;
|
||||||
height_ = e.window.data2;
|
height_ = e.window.data2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Map input to commands
|
// Map input to commands
|
||||||
input_.ProcessSDLEvent(e);
|
input_.ProcessSDLEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute pending mapped inputs (drain queue)
|
// Execute pending mapped inputs (drain queue)
|
||||||
for (;;) {
|
for (;;) {
|
||||||
MappedInput mi;
|
MappedInput mi;
|
||||||
if (!input_.Poll(mi))
|
if (!input_.Poll(mi))
|
||||||
break;
|
break;
|
||||||
if (mi.hasCommand) {
|
if (mi.hasCommand) {
|
||||||
Execute(ed, mi.id, mi.arg, mi.count);
|
Execute(ed, mi.id, mi.arg, mi.count);
|
||||||
if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) {
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new ImGui frame
|
if (ed.QuitRequested()) {
|
||||||
ImGui_ImplOpenGL3_NewFrame();
|
running = false;
|
||||||
ImGui_ImplSDL2_NewFrame(window_);
|
}
|
||||||
ImGui::NewFrame();
|
|
||||||
|
|
||||||
// Update editor logical rows/cols using current ImGui metrics and display size
|
// Start a new ImGui frame
|
||||||
{
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGui_ImplSDL2_NewFrame(window_);
|
||||||
float line_h = ImGui::GetTextLineHeightWithSpacing();
|
ImGui::NewFrame();
|
||||||
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<float>(width_);
|
|
||||||
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
|
|
||||||
|
|
||||||
// Account for the GUI window padding and the status bar height used in GUIRenderer
|
// Update editor logical rows/cols using current ImGui metrics and display size
|
||||||
const ImGuiStyle &style = ImGui::GetStyle();
|
{
|
||||||
float pad_x = style.WindowPadding.x;
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
float pad_y = style.WindowPadding.y;
|
float line_h = ImGui::GetTextLineHeightWithSpacing();
|
||||||
// Status bar reserves one frame height (with spacing) inside the window
|
float ch_w = ImGui::CalcTextSize("M").x;
|
||||||
float status_h = ImGui::GetFrameHeightWithSpacing();
|
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_);
|
||||||
|
|
||||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
// Account for the GUI window padding and the status bar height used in GUIRenderer
|
||||||
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
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();
|
||||||
|
|
||||||
// Visible content rows inside the scroll child
|
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
||||||
std::size_t content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
|
||||||
// Editor::Rows includes the status line; add 1 back for it.
|
|
||||||
std::size_t rows = std::max<std::size_t>(1, content_rows + 1);
|
|
||||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
|
||||||
|
|
||||||
// Only update if changed to avoid churn
|
// Visible content rows inside the scroll child
|
||||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
std::size_t content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
|
||||||
ed.SetDimensions(rows, cols);
|
// Editor::Rows includes the status line; add 1 back for it.
|
||||||
}
|
std::size_t rows = std::max<std::size_t>(1, content_rows + 1);
|
||||||
}
|
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
||||||
|
|
||||||
// No runtime font UI; always use embedded font.
|
// Only update if changed to avoid churn
|
||||||
|
if (rows != ed.Rows() || cols != ed.Cols()) {
|
||||||
|
ed.SetDimensions(rows, cols);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw editor UI
|
// No runtime font UI; always use embedded font.
|
||||||
renderer_.Draw(ed);
|
|
||||||
|
// Draw editor UI
|
||||||
|
renderer_.Draw(ed);
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
ImGui::Render();
|
ImGui::Render();
|
||||||
@@ -184,18 +187,19 @@ GUIFrontend::Shutdown()
|
|||||||
bool
|
bool
|
||||||
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
|
||||||
{
|
{
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
io.Fonts->Clear();
|
io.Fonts->Clear();
|
||||||
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||||
(void*)DefaultFontRegularCompressedData,
|
(void *) DefaultFontRegularCompressedData,
|
||||||
(int)DefaultFontRegularCompressedSize,
|
(int) DefaultFontRegularCompressedSize,
|
||||||
size_px);
|
size_px);
|
||||||
if (!font) {
|
if (!font) {
|
||||||
font = io.Fonts->AddFontDefault();
|
font = io.Fonts->AddFontDefault();
|
||||||
}
|
}
|
||||||
(void) font;
|
(void) font;
|
||||||
io.Fonts->Build();
|
io.Fonts->Build();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No runtime font reload or system font resolution in this simplified build.
|
|
||||||
|
// No runtime font reload or system font resolution in this simplified build.
|
||||||
|
|||||||
@@ -13,25 +13,25 @@ typedef void *SDL_GLContext;
|
|||||||
|
|
||||||
class GUIFrontend final : public Frontend {
|
class GUIFrontend final : public Frontend {
|
||||||
public:
|
public:
|
||||||
GUIFrontend() = default;
|
GUIFrontend() = default;
|
||||||
|
|
||||||
~GUIFrontend() override = default;
|
~GUIFrontend() override = default;
|
||||||
|
|
||||||
bool Init(Editor &ed) override;
|
bool Init(Editor &ed) override;
|
||||||
|
|
||||||
void Step(Editor &ed, bool &running) override;
|
void Step(Editor &ed, bool &running) override;
|
||||||
|
|
||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool LoadGuiFont_(const char *path, float size_px);
|
bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIInputHandler input_{};
|
GUIInputHandler input_{};
|
||||||
GUIRenderer renderer_{};
|
GUIRenderer renderer_{};
|
||||||
SDL_Window *window_ = nullptr;
|
SDL_Window *window_ = nullptr;
|
||||||
SDL_GLContext gl_ctx_ = nullptr;
|
SDL_GLContext gl_ctx_ = nullptr;
|
||||||
int width_ = 1280;
|
int width_ = 1280;
|
||||||
int height_ = 800;
|
int height_ = 800;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_GUI_FRONTEND_H
|
#endif // KTE_GUI_FRONTEND_H
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
static bool
|
static bool
|
||||||
map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput &out)
|
map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput &out)
|
||||||
{
|
{
|
||||||
// Ctrl handling
|
// Ctrl handling
|
||||||
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
||||||
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||||
|
|
||||||
// Movement and basic keys
|
// Movement and basic keys
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case SDLK_LEFT:
|
case SDLK_LEFT:
|
||||||
out = {true, CommandId::MoveLeft, "", 0};
|
out = {true, CommandId::MoveLeft, "", 0};
|
||||||
return true;
|
return true;
|
||||||
@@ -28,21 +28,28 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
|||||||
case SDLK_HOME:
|
case SDLK_HOME:
|
||||||
out = {true, CommandId::MoveHome, "", 0};
|
out = {true, CommandId::MoveHome, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_END:
|
case SDLK_END:
|
||||||
out = {true, CommandId::MoveEnd, "", 0};
|
out = {true, CommandId::MoveEnd, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_PAGEUP:
|
case SDLK_PAGEUP:
|
||||||
out = {true, CommandId::PageUp, "", 0};
|
out = {true, CommandId::PageUp, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_PAGEDOWN:
|
case SDLK_PAGEDOWN:
|
||||||
out = {true, CommandId::PageDown, "", 0};
|
out = {true, CommandId::PageDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_DELETE:
|
case SDLK_DELETE:
|
||||||
out = {true, CommandId::DeleteChar, "", 0};
|
out = {true, CommandId::DeleteChar, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_BACKSPACE:
|
case SDLK_BACKSPACE:
|
||||||
out = {true, CommandId::Backspace, "", 0};
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
return true;
|
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_RETURN:
|
||||||
case SDLK_KP_ENTER:
|
case SDLK_KP_ENTER:
|
||||||
out = {true, CommandId::Newline, "", 0};
|
out = {true, CommandId::Newline, "", 0};
|
||||||
@@ -55,90 +62,59 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_ctrl) {
|
if (is_ctrl) {
|
||||||
switch (key) {
|
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
|
||||||
case SDLK_k:
|
k_prefix = true;
|
||||||
case SDLK_KP_EQUALS: // treat Ctrl-K
|
out = {true, CommandId::KPrefix, "", 0};
|
||||||
k_prefix = true;
|
return true;
|
||||||
out = {true, CommandId::KPrefix, "", 0};
|
}
|
||||||
return true;
|
// Map other control chords via shared keymap
|
||||||
case SDLK_n: // C-n: down
|
if (key >= SDLK_a && key <= SDLK_z) {
|
||||||
out = {true, CommandId::MoveDown, "", 0};
|
int ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
||||||
return true;
|
CommandId id;
|
||||||
case SDLK_p: // C-p: up
|
if (KLookupCtrlCommand(ascii_key, id)) {
|
||||||
out = {true, CommandId::MoveUp, "", 0};
|
out = {true, id, "", 0};
|
||||||
return true;
|
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)
|
// Alt/Meta bindings (ESC f/b equivalent)
|
||||||
if (is_alt) {
|
if (is_alt) {
|
||||||
switch (key) {
|
if (key >= SDLK_a && key <= SDLK_z) {
|
||||||
case SDLK_b:
|
int ascii_key = static_cast<int>('a' + (key - SDLK_a));
|
||||||
out = {true, CommandId::WordPrev, "", 0};
|
CommandId id;
|
||||||
return true;
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
case SDLK_f:
|
out = {true, id, "", 0};
|
||||||
out = {true, CommandId::WordNext, "", 0};
|
return true;
|
||||||
return true;
|
}
|
||||||
default:
|
}
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (k_prefix) {
|
if (k_prefix) {
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
// Normalize SDL key to ASCII where possible
|
// Normalize SDL key to ASCII where possible
|
||||||
int ascii_key = 0;
|
int ascii_key = 0;
|
||||||
if (key >= SDLK_SPACE && key <= SDLK_z) {
|
if (key >= SDLK_SPACE && key <= SDLK_z) {
|
||||||
ascii_key = static_cast<int>(key);
|
ascii_key = static_cast<int>(key);
|
||||||
}
|
}
|
||||||
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
||||||
if (ascii_key != 0) {
|
if (ascii_key != 0) {
|
||||||
ascii_key = KLowerAscii(ascii_key);
|
ascii_key = KLowerAscii(ascii_key);
|
||||||
CommandId id;
|
CommandId id;
|
||||||
if (KLookupKCommand(ascii_key, ctrl2, id)) {
|
if (KLookupKCommand(ascii_key, ctrl2, id)) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Unknown k-command: report the typed character
|
// Unknown k-command: report the typed character
|
||||||
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
||||||
std::string arg(1, c);
|
std::string arg(1, c);
|
||||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
out.hasCommand = false;
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -150,16 +126,60 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
MappedInput mi;
|
MappedInput mi;
|
||||||
bool produced = false;
|
bool produced = false;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case SDL_KEYDOWN:
|
case SDL_KEYDOWN: {
|
||||||
produced = map_key(e.key.keysym.sym, SDL_Keymod(e.key.keysym.mod), k_prefix_, mi);
|
// Remember whether we were in k-prefix before handling this key
|
||||||
break;
|
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:
|
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.hasCommand = true;
|
||||||
mi.id = CommandId::InsertText;
|
mi.id = CommandId::InsertText;
|
||||||
mi.arg = std::string(e.text.text);
|
mi.arg = std::string(e.text.text);
|
||||||
mi.count = 0;
|
mi.count = 0;
|
||||||
produced = true;
|
produced = true;
|
||||||
|
} else {
|
||||||
|
produced = true; // consumed while k-prefix is active
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ private:
|
|||||||
std::mutex mu_;
|
std::mutex mu_;
|
||||||
std::queue<MappedInput> q_;
|
std::queue<MappedInput> q_;
|
||||||
bool k_prefix_ = false;
|
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
|
#endif // KTE_GUI_INPUT_HANDLER_H
|
||||||
|
|||||||
315
GUIRenderer.cc
315
GUIRenderer.cc
@@ -56,77 +56,81 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
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;
|
||||||
// If the command layer requested a specific top-of-screen (via Buffer::Rowoffs),
|
// 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.
|
// force the ImGui scroll to match so paging aligns the first visible row.
|
||||||
bool forced_scroll = false;
|
bool forced_scroll = false;
|
||||||
{
|
{
|
||||||
std::size_t desired_top = buf->Rowoffs();
|
std::size_t desired_top = buf->Rowoffs();
|
||||||
long current_top = static_cast<long>(scroll_y / row_h);
|
long current_top = static_cast<long>(scroll_y / row_h);
|
||||||
if (static_cast<long>(desired_top) != current_top) {
|
if (static_cast<long>(desired_top) != current_top) {
|
||||||
ImGui::SetScrollY(static_cast<float>(desired_top) * row_h);
|
ImGui::SetScrollY(static_cast<float>(desired_top) * row_h);
|
||||||
scroll_y = ImGui::GetScrollY();
|
scroll_y = ImGui::GetScrollY();
|
||||||
forced_scroll = true;
|
forced_scroll = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Synchronize cursor and scrolling.
|
// Synchronize cursor and scrolling.
|
||||||
// A) When the user scrolls and the cursor goes off-screen, move the cursor to the nearest visible row.
|
// 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.
|
// B) When the cursor moves (via keyboard commands), scroll it back into view.
|
||||||
{
|
{
|
||||||
static float prev_scroll_y = -1.0f;
|
static float prev_scroll_y = -1.0f;
|
||||||
static long prev_cursor_y = -1;
|
static long prev_cursor_y = -1;
|
||||||
// Compute visible row range using the child window height
|
// Compute visible row range using the child window height
|
||||||
float child_h = ImGui::GetWindowHeight();
|
float child_h = ImGui::GetWindowHeight();
|
||||||
long first_row = static_cast<long>(scroll_y / row_h);
|
long first_row = static_cast<long>(scroll_y / row_h);
|
||||||
long vis_rows = static_cast<long>(child_h / row_h);
|
long vis_rows = static_cast<long>(child_h / row_h);
|
||||||
if (vis_rows < 1) vis_rows = 1;
|
if (vis_rows < 1)
|
||||||
long last_row = first_row + 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
|
// 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) {
|
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||||
long cyr = static_cast<long>(cy);
|
long cyr = static_cast<long>(cy);
|
||||||
if (cyr < first_row || cyr > last_row) {
|
if (cyr < first_row || cyr > last_row) {
|
||||||
long new_row = (cyr < first_row) ? first_row : last_row;
|
long new_row = (cyr < first_row) ? first_row : last_row;
|
||||||
if (new_row < 0) new_row = 0;
|
if (new_row < 0)
|
||||||
if (new_row >= static_cast<long>(lines.size()))
|
new_row = 0;
|
||||||
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
|
if (new_row >= static_cast<long>(lines.size()))
|
||||||
// Clamp column to line length
|
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
|
||||||
std::size_t new_col = 0;
|
// Clamp column to line length
|
||||||
if (!lines.empty()) {
|
std::size_t new_col = 0;
|
||||||
const std::string &l = lines[static_cast<std::size_t>(new_row)];
|
if (!lines.empty()) {
|
||||||
new_col = std::min<std::size_t>(cx, l.size());
|
const std::string &l = lines[static_cast<std::size_t>(new_row)];
|
||||||
}
|
new_col = std::min<std::size_t>(cx, l.size());
|
||||||
char tmp2[64];
|
}
|
||||||
std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col);
|
char tmp2[64];
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp2));
|
std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col);
|
||||||
cy = buf->Cury();
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp2));
|
||||||
cx = buf->Curx();
|
cy = buf->Cury();
|
||||||
cyr = static_cast<long>(cy);
|
cx = buf->Curx();
|
||||||
// Update visible range again in case content changed
|
cyr = static_cast<long>(cy);
|
||||||
first_row = static_cast<long>(ImGui::GetScrollY() / row_h);
|
// Update visible range again in case content changed
|
||||||
last_row = first_row + vis_rows - 1;
|
first_row = static_cast<long>(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
|
// 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.
|
// Skip this when we just forced a top-of-screen alignment this frame.
|
||||||
if (!forced_scroll && prev_cursor_y >= 0 && static_cast<long>(cy) != prev_cursor_y) {
|
if (!forced_scroll && prev_cursor_y >= 0 && static_cast<long>(cy) != prev_cursor_y) {
|
||||||
long cyr = static_cast<long>(cy);
|
long cyr = static_cast<long>(cy);
|
||||||
if (cyr < first_row || cyr > last_row) {
|
if (cyr < first_row || cyr > last_row) {
|
||||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||||
float max_y = ImGui::GetScrollMaxY();
|
float max_y = ImGui::GetScrollMaxY();
|
||||||
if (target < 0.f) target = 0.f;
|
if (target < 0.f)
|
||||||
if (max_y >= 0.f && target > max_y) target = max_y;
|
target = 0.f;
|
||||||
ImGui::SetScrollY(target);
|
if (max_y >= 0.f && target > max_y)
|
||||||
// refresh local variables
|
target = max_y;
|
||||||
scroll_y = ImGui::GetScrollY();
|
ImGui::SetScrollY(target);
|
||||||
first_row = static_cast<long>(scroll_y / row_h);
|
// refresh local variables
|
||||||
last_row = first_row + vis_rows - 1;
|
scroll_y = ImGui::GetScrollY();
|
||||||
}
|
first_row = static_cast<long>(scroll_y / row_h);
|
||||||
}
|
last_row = first_row + vis_rows - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
prev_scroll_y = ImGui::GetScrollY();
|
prev_scroll_y = ImGui::GetScrollY();
|
||||||
prev_cursor_y = static_cast<long>(cy);
|
prev_cursor_y = static_cast<long>(cy);
|
||||||
}
|
}
|
||||||
// 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;
|
||||||
@@ -190,98 +194,103 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
|
||||||
// Status bar spanning full width
|
// Status bar spanning full width
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// Build three segments: left (app/version/buffer/dirty), middle (message), right (cursor/mark)
|
// Build three segments: left (app/version/buffer/dirty), middle (message), right (cursor/mark)
|
||||||
// Compute full content width and draw a filled background rectangle
|
// Compute full content width and draw a filled background rectangle
|
||||||
ImVec2 win_pos = ImGui::GetWindowPos();
|
ImVec2 win_pos = ImGui::GetWindowPos();
|
||||||
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
|
||||||
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
|
||||||
float x0 = win_pos.x + cr_min.x;
|
float x0 = win_pos.x + cr_min.x;
|
||||||
float x1 = win_pos.x + cr_max.x;
|
float x1 = win_pos.x + cr_max.x;
|
||||||
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
||||||
float bar_h = ImGui::GetFrameHeight();
|
float bar_h = ImGui::GetFrameHeight();
|
||||||
ImVec2 p0(x0, cursor.y);
|
ImVec2 p0(x0, cursor.y);
|
||||||
ImVec2 p1(x1, cursor.y + bar_h);
|
ImVec2 p1(x1, cursor.y + bar_h);
|
||||||
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
|
||||||
// Build left text
|
// Build left text
|
||||||
std::string left;
|
std::string left;
|
||||||
left.reserve(256);
|
left.reserve(256);
|
||||||
left += "kge"; // GUI app name
|
left += "kge"; // GUI app name
|
||||||
left += " ";
|
left += " ";
|
||||||
left += KTE_VERSION_STR;
|
left += KTE_VERSION_STR;
|
||||||
std::string fname = buf->Filename();
|
std::string fname = buf->Filename();
|
||||||
if (!fname.empty()) {
|
if (!fname.empty()) {
|
||||||
try { fname = std::filesystem::path(fname).filename().string(); } catch (...) {}
|
try {
|
||||||
} else {
|
fname = std::filesystem::path(fname).filename().string();
|
||||||
fname = "[no name]";
|
} catch (...) {}
|
||||||
}
|
} else {
|
||||||
left += " ";
|
fname = "[no name]";
|
||||||
left += fname;
|
}
|
||||||
if (buf->Dirty()) left += " *";
|
left += " ";
|
||||||
|
left += fname;
|
||||||
|
if (buf->Dirty())
|
||||||
|
left += " *";
|
||||||
|
|
||||||
// Build right text (cursor/mark)
|
// Build right text (cursor/mark)
|
||||||
int row1 = static_cast<int>(buf->Cury()) + 1;
|
int row1 = static_cast<int>(buf->Cury()) + 1;
|
||||||
int col1 = static_cast<int>(buf->Curx()) + 1;
|
int col1 = static_cast<int>(buf->Curx()) + 1;
|
||||||
bool have_mark = buf->MarkSet();
|
bool have_mark = buf->MarkSet();
|
||||||
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
||||||
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
||||||
char rbuf[128];
|
char rbuf[128];
|
||||||
if (have_mark) std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
if (have_mark)
|
||||||
else std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
||||||
std::string right = rbuf;
|
else
|
||||||
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||||
|
std::string right = rbuf;
|
||||||
|
|
||||||
// Middle message
|
// Middle message
|
||||||
const std::string &msg = ed.Status();
|
const std::string &msg = ed.Status();
|
||||||
|
|
||||||
// Measurements
|
// Measurements
|
||||||
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
||||||
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
ImVec2 right_sz = ImGui::CalcTextSize(right.c_str());
|
||||||
float pad = 6.f;
|
float pad = 6.f;
|
||||||
float left_x = p0.x + pad;
|
float left_x = p0.x + pad;
|
||||||
float right_x = p1.x - pad - right_sz.x;
|
float right_x = p1.x - pad - right_sz.x;
|
||||||
if (right_x < left_x + left_sz.x + pad) {
|
if (right_x < left_x + left_sz.x + pad) {
|
||||||
// Not enough room; clip left to fit
|
// Not enough room; clip left to fit
|
||||||
float max_left = std::max(0.0f, right_x - left_x - pad);
|
float max_left = std::max(0.0f, right_x - left_x - pad);
|
||||||
if (max_left < left_sz.x && max_left > 10.0f) {
|
if (max_left < left_sz.x && max_left > 10.0f) {
|
||||||
// Render a clipped left using a child region
|
// Render a clipped left using a child region
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||||
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true);
|
||||||
ImGui::TextUnformatted(left.c_str());
|
ImGui::TextUnformatted(left.c_str());
|
||||||
ImGui::PopClipRect();
|
ImGui::PopClipRect();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Draw left normally
|
// Draw left normally
|
||||||
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f));
|
||||||
ImGui::TextUnformatted(left.c_str());
|
ImGui::TextUnformatted(left.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw right
|
// Draw right
|
||||||
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f));
|
||||||
ImGui::TextUnformatted(right.c_str());
|
ImGui::TextUnformatted(right.c_str());
|
||||||
|
|
||||||
// Draw middle message centered in remaining space
|
// Draw middle message centered in remaining space
|
||||||
if (!msg.empty()) {
|
if (!msg.empty()) {
|
||||||
float mid_left = left_x + left_sz.x + pad;
|
float mid_left = left_x + left_sz.x + pad;
|
||||||
float mid_right = std::max(right_x - pad, mid_left);
|
float mid_right = std::max(right_x - pad, mid_left);
|
||||||
float mid_w = std::max(0.0f, mid_right - mid_left);
|
float mid_w = std::max(0.0f, mid_right - mid_left);
|
||||||
if (mid_w > 1.0f) {
|
if (mid_w > 1.0f) {
|
||||||
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
|
||||||
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f);
|
||||||
// Clip to middle region
|
// Clip to middle region
|
||||||
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true);
|
||||||
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
|
||||||
ImGui::TextUnformatted(msg.c_str());
|
ImGui::TextUnformatted(msg.c_str());
|
||||||
ImGui::PopClipRect();
|
ImGui::PopClipRect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Advance cursor to after the bar to keep layout consistent
|
// Advance cursor to after the bar to keep layout consistent
|
||||||
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
ImGui::PopStyleVar(3);
|
ImGui::PopStyleVar(3);
|
||||||
|
|||||||
100
KKeymap.cc
100
KKeymap.cc
@@ -13,28 +13,88 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
out = CommandId::SaveAndQuit;
|
out = CommandId::SaveAndQuit;
|
||||||
return true; // C-k C-x
|
return true; // C-k C-x
|
||||||
case 'q':
|
case 'q':
|
||||||
out = CommandId::Quit;
|
out = CommandId::QuitNow;
|
||||||
return true; // C-k C-q (quit immediately)
|
return true; // C-k C-q (quit immediately)
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case 's':
|
case 's':
|
||||||
out = CommandId::Save;
|
out = CommandId::Save;
|
||||||
return true; // C-k s
|
return true; // C-k s
|
||||||
case 'e':
|
case 'e':
|
||||||
out = CommandId::OpenFileStart;
|
out = CommandId::OpenFileStart;
|
||||||
return true; // C-k e (open file)
|
return true; // C-k e (open file)
|
||||||
case 'x':
|
case 'x':
|
||||||
out = CommandId::SaveAndQuit;
|
out = CommandId::SaveAndQuit;
|
||||||
return true; // C-k x
|
return true; // C-k x
|
||||||
case 'q':
|
case 'q':
|
||||||
out = CommandId::Quit;
|
out = CommandId::Quit;
|
||||||
return true; // C-k q
|
return true; // C-k q
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
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.
|
// Returns true and sets out if a mapping exists; false otherwise.
|
||||||
bool KLookupKCommand(int ascii_key, bool ctrl, CommandId &out);
|
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.
|
// Utility: normalize an int keycode to lowercased ASCII if it's in printable range.
|
||||||
inline int
|
inline int
|
||||||
KLowerAscii(const int key)
|
KLowerAscii(const int key)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <termios.h>
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
@@ -9,21 +10,29 @@
|
|||||||
bool
|
bool
|
||||||
TerminalFrontend::Init(Editor &ed)
|
TerminalFrontend::Init(Editor &ed)
|
||||||
{
|
{
|
||||||
initscr();
|
// Ensure Ctrl-S/Ctrl-Q reach the application by disabling XON/XOFF flow control
|
||||||
cbreak();
|
{
|
||||||
noecho();
|
struct termios tio{};
|
||||||
keypad(stdscr, TRUE);
|
if (tcgetattr(STDIN_FILENO, &tio) == 0) {
|
||||||
// Enable 8-bit meta key sequences (Alt/ESC-prefix handling in terminals)
|
tio.c_iflag &= static_cast<unsigned long>(~IXON);
|
||||||
meta(stdscr, TRUE);
|
(void) tcsetattr(STDIN_FILENO, TCSANOW, &tio);
|
||||||
// Make ESC key sequences resolve quickly so ESC+<key> works as meta
|
}
|
||||||
|
}
|
||||||
|
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+<key> works as meta
|
||||||
#ifdef set_escdelay
|
#ifdef set_escdelay
|
||||||
set_escdelay(50);
|
set_escdelay(50);
|
||||||
#endif
|
#endif
|
||||||
nodelay(stdscr, TRUE);
|
nodelay(stdscr, TRUE);
|
||||||
curs_set(1);
|
curs_set(1);
|
||||||
// Enable mouse support if available
|
// Enable mouse support if available
|
||||||
mouseinterval(0);
|
mouseinterval(0);
|
||||||
mousemask(ALL_MOUSE_EVENTS, nullptr);
|
mousemask(ALL_MOUSE_EVENTS, nullptr);
|
||||||
|
|
||||||
int r = 0, c = 0;
|
int r = 0, c = 0;
|
||||||
getmaxyx(stdscr, r, c);
|
getmaxyx(stdscr, r, c);
|
||||||
@@ -52,15 +61,16 @@ TerminalFrontend::Step(Editor &ed, bool &running)
|
|||||||
if (input_.Poll(mi)) {
|
if (input_.Poll(mi)) {
|
||||||
if (mi.hasCommand) {
|
if (mi.hasCommand) {
|
||||||
Execute(ed, mi.id, mi.arg, mi.count);
|
Execute(ed, mi.id, mi.arg, mi.count);
|
||||||
if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) {
|
|
||||||
running = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Avoid busy loop
|
// Avoid busy loop
|
||||||
usleep(1000);
|
usleep(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ed.QuitRequested()) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
renderer_.Draw(ed);
|
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};
|
out = {true, CommandId::Refresh, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (ch == CTRL('L')) {
|
// Tab (note: terminals encode Tab and C-i as the same code 9)
|
||||||
out = {true, CommandId::Refresh, "", 0};
|
if (ch == '\t') {
|
||||||
|
k_prefix = false;
|
||||||
|
out.hasCommand = true;
|
||||||
|
out.id = CommandId::InsertText;
|
||||||
|
out.arg = "\t";
|
||||||
|
out.count = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (ch == CTRL('S')) {
|
// Generic Control-chord lookup (after handling special prefixes/cancel)
|
||||||
out = {true, CommandId::FindStart, "", 0};
|
if (ch >= 1 && ch <= 26) {
|
||||||
return true;
|
int ascii_key = 'a' + (ch - 1);
|
||||||
}
|
CommandId id;
|
||||||
// Emacs-style movement aliases
|
if (KLookupCtrlCommand(ascii_key, id)) {
|
||||||
if (ch == CTRL('N')) { // C-n: down
|
out = {true, id, "", 0};
|
||||||
out = {true, CommandId::MoveDown, "", 0};
|
return true;
|
||||||
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
|
// Enter
|
||||||
@@ -142,18 +127,15 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
|||||||
return true;
|
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) {
|
if (esc_meta) {
|
||||||
esc_meta = false;
|
esc_meta = false;
|
||||||
int ascii_key = ch;
|
int ascii_key = ch;
|
||||||
if (ascii_key >= 'A' && ascii_key <= 'Z')
|
if (ascii_key >= 'A' && ascii_key <= 'Z')
|
||||||
ascii_key = ascii_key - 'A' + 'a';
|
ascii_key = ascii_key - 'A' + 'a';
|
||||||
if (ascii_key == 'b') {
|
CommandId id;
|
||||||
out = {true, CommandId::WordPrev, "", 0};
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
return true;
|
out = {true, id, "", 0};
|
||||||
}
|
|
||||||
if (ascii_key == 'f') {
|
|
||||||
out = {true, CommandId::WordNext, "", 0};
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Unhandled meta key: no command
|
// Unhandled meta key: no command
|
||||||
@@ -161,29 +143,29 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k_prefix) {
|
if (k_prefix) {
|
||||||
k_prefix = false; // single next key only
|
k_prefix = false; // single next key only
|
||||||
// Determine if this is a control chord (e.g., C-x) and normalize
|
// Determine if this is a control chord (e.g., C-x) and normalize
|
||||||
bool ctrl = false;
|
bool ctrl = false;
|
||||||
int ascii_key = ch;
|
int ascii_key = ch;
|
||||||
if (ch >= 1 && ch <= 26) {
|
if (ch >= 1 && ch <= 26) {
|
||||||
ctrl = true;
|
ctrl = true;
|
||||||
ascii_key = 'a' + (ch - 1);
|
ascii_key = 'a' + (ch - 1);
|
||||||
}
|
}
|
||||||
// For letters, normalize to lowercase ASCII
|
// For letters, normalize to lowercase ASCII
|
||||||
ascii_key = KLowerAscii(ascii_key);
|
ascii_key = KLowerAscii(ascii_key);
|
||||||
|
|
||||||
CommandId id;
|
CommandId id;
|
||||||
if (KLookupKCommand(ascii_key, ctrl, id)) {
|
if (KLookupKCommand(ascii_key, ctrl, id)) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
} else {
|
} else {
|
||||||
// Show unknown k-command message with the typed character
|
// Show unknown k-command message with the typed character
|
||||||
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast<char>(ascii_key) : '?';
|
||||||
std::string arg(1, c);
|
std::string arg(1, c);
|
||||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable ASCII
|
// Printable ASCII
|
||||||
if (ch >= 0x20 && ch <= 0x7E) {
|
if (ch >= 0x20 && ch <= 0x7E) {
|
||||||
|
|||||||
@@ -11,19 +11,19 @@
|
|||||||
|
|
||||||
class TerminalInputHandler final : public InputHandler {
|
class TerminalInputHandler final : public InputHandler {
|
||||||
public:
|
public:
|
||||||
TerminalInputHandler();
|
TerminalInputHandler();
|
||||||
|
|
||||||
~TerminalInputHandler() override;
|
~TerminalInputHandler() override;
|
||||||
|
|
||||||
bool Poll(MappedInput &out) override;
|
bool Poll(MappedInput &out) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool decode_(MappedInput &out);
|
bool decode_(MappedInput &out);
|
||||||
|
|
||||||
// ke-style prefix state
|
// ke-style prefix state
|
||||||
bool k_prefix_ = false; // true after C-k until next key or ESC
|
bool k_prefix_ = false; // true after C-k until next key or ESC
|
||||||
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
||||||
bool esc_meta_ = false;
|
bool esc_meta_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
#endif // KTE_TERMINAL_INPUT_HANDLER_H
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
int rows, cols;
|
int rows, cols;
|
||||||
getmaxyx(stdscr, rows, cols);
|
getmaxyx(stdscr, rows, cols);
|
||||||
|
|
||||||
// Clear screen
|
// Clear screen
|
||||||
erase();
|
erase();
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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) {
|
||||||
const auto &lines = buf->Rows();
|
const auto &lines = buf->Rows();
|
||||||
std::size_t rowoffs = buf->Rowoffs();
|
std::size_t rowoffs = buf->Rowoffs();
|
||||||
std::size_t coloffs = buf->Coloffs();
|
std::size_t coloffs = buf->Coloffs();
|
||||||
@@ -139,113 +139,117 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
std::size_t rx = buf->Rx(); // render x computed by command layer
|
std::size_t rx = buf->Rx(); // render x computed by command layer
|
||||||
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
||||||
int cur_x = static_cast<int>(rx) - static_cast<int>(buf->Coloffs());
|
int cur_x = static_cast<int>(rx) - static_cast<int>(buf->Coloffs());
|
||||||
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
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
|
// remember where to leave the terminal cursor after status is drawn
|
||||||
saved_cur_y = cur_y;
|
saved_cur_y = cur_y;
|
||||||
saved_cur_x = cur_x;
|
saved_cur_x = cur_x;
|
||||||
move(cur_y, cur_x);
|
move(cur_y, cur_x);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mvaddstr(0, 0, "[no buffer]");
|
mvaddstr(0, 0, "[no buffer]");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark
|
// Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark
|
||||||
move(rows - 1, 0);
|
move(rows - 1, 0);
|
||||||
attron(A_REVERSE);
|
attron(A_REVERSE);
|
||||||
|
|
||||||
// Fill the status line with spaces first
|
// 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
|
// Build left segment
|
||||||
std::string left;
|
std::string left;
|
||||||
{
|
{
|
||||||
const char *app = "kte";
|
const char *app = "kte";
|
||||||
left.reserve(256);
|
left.reserve(256);
|
||||||
left += app;
|
left += app;
|
||||||
left += " ";
|
left += " ";
|
||||||
left += KTE_VERSION_STR; // already includes leading 'v'
|
left += KTE_VERSION_STR; // already includes leading 'v'
|
||||||
const Buffer *b = buf;
|
const Buffer *b = buf;
|
||||||
std::string fname;
|
std::string fname;
|
||||||
if (b) {
|
if (b) {
|
||||||
fname = b->Filename();
|
fname = b->Filename();
|
||||||
}
|
}
|
||||||
if (!fname.empty()) {
|
if (!fname.empty()) {
|
||||||
try {
|
try {
|
||||||
fname = std::filesystem::path(fname).filename().string();
|
fname = std::filesystem::path(fname).filename().string();
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// keep original on any error
|
// keep original on any error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fname = "[no name]";
|
fname = "[no name]";
|
||||||
}
|
}
|
||||||
left += " ";
|
left += " ";
|
||||||
left += fname;
|
left += fname;
|
||||||
if (b && b->Dirty())
|
if (b && b->Dirty())
|
||||||
left += " *";
|
left += " *";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build right segment (cursor and mark)
|
// Build right segment (cursor and mark)
|
||||||
std::string right;
|
std::string right;
|
||||||
{
|
{
|
||||||
int row1 = 0, col1 = 0;
|
int row1 = 0, col1 = 0;
|
||||||
int mrow1 = 0, mcol1 = 0;
|
int mrow1 = 0, mcol1 = 0;
|
||||||
bool have_mark = false;
|
bool have_mark = false;
|
||||||
if (buf) {
|
if (buf) {
|
||||||
row1 = static_cast<int>(buf->Cury()) + 1;
|
row1 = static_cast<int>(buf->Cury()) + 1;
|
||||||
col1 = static_cast<int>(buf->Curx()) + 1;
|
col1 = static_cast<int>(buf->Curx()) + 1;
|
||||||
if (buf->MarkSet()) {
|
if (buf->MarkSet()) {
|
||||||
have_mark = true;
|
have_mark = true;
|
||||||
mrow1 = static_cast<int>(buf->MarkCury()) + 1;
|
mrow1 = static_cast<int>(buf->MarkCury()) + 1;
|
||||||
mcol1 = static_cast<int>(buf->MarkCurx()) + 1;
|
mcol1 = static_cast<int>(buf->MarkCurx()) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
char rbuf[128];
|
char rbuf[128];
|
||||||
if (have_mark)
|
if (have_mark)
|
||||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1);
|
||||||
else
|
else
|
||||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||||
right = rbuf;
|
right = rbuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute placements with truncation rules: prioritize left and right; middle gets remaining
|
// Compute placements with truncation rules: prioritize left and right; middle gets remaining
|
||||||
int rlen = static_cast<int>(right.size());
|
int rlen = static_cast<int>(right.size());
|
||||||
if (rlen > cols) {
|
if (rlen > cols) {
|
||||||
// Hard clip right if too long
|
// Hard clip right if too long
|
||||||
right = right.substr(static_cast<std::size_t>(rlen - cols), static_cast<std::size_t>(cols));
|
right = right.substr(static_cast<std::size_t>(rlen - cols), static_cast<std::size_t>(cols));
|
||||||
rlen = cols;
|
rlen = cols;
|
||||||
}
|
}
|
||||||
int left_max = std::max(0, cols - rlen - 1); // leave at least 1 space between left and right areas
|
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());
|
int llen = static_cast<int>(left.size());
|
||||||
if (llen > left_max) llen = left_max;
|
if (llen > left_max)
|
||||||
|
llen = left_max;
|
||||||
|
|
||||||
// Draw left
|
// 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
|
// Draw right, flush to end
|
||||||
int rstart = std::max(0, cols - rlen);
|
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
|
// Middle message
|
||||||
const std::string &msg = ed.Status();
|
const std::string &msg = ed.Status();
|
||||||
if (!msg.empty()) {
|
if (!msg.empty()) {
|
||||||
int mid_start = llen + 1; // one space after left
|
int mid_start = llen + 1; // one space after left
|
||||||
int mid_end = rstart - 1; // one space before right
|
int mid_end = rstart - 1; // one space before right
|
||||||
if (mid_end >= mid_start) {
|
if (mid_end >= mid_start) {
|
||||||
int avail = mid_end - mid_start + 1;
|
int avail = mid_end - mid_start + 1;
|
||||||
int mlen = static_cast<int>(msg.size());
|
int mlen = static_cast<int>(msg.size());
|
||||||
int mdraw = std::min(avail, mlen);
|
int mdraw = std::min(avail, mlen);
|
||||||
int mstart = mid_start + std::max(0, (avail - mdraw) / 2); // center within middle area
|
int mstart = mid_start + std::max(0, (avail - mdraw) / 2); // center within middle area
|
||||||
mvaddnstr(rows - 1, mstart, msg.c_str(), mdraw);
|
mvaddnstr(rows - 1, mstart, msg.c_str(), mdraw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attroff(A_REVERSE);
|
attroff(A_REVERSE);
|
||||||
|
|
||||||
// Restore terminal cursor to the content position so a visible caret
|
// Restore terminal cursor to the content position so a visible caret
|
||||||
// remains in the editing area (not on the status line).
|
// remains in the editing area (not on the status line).
|
||||||
if (saved_cur_y >= 0 && saved_cur_x >= 0) {
|
if (saved_cur_y >= 0 && saved_cur_x >= 0) {
|
||||||
move(saved_cur_y, saved_cur_x);
|
move(saved_cur_y, saved_cur_x);
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
|
|
||||||
class TerminalRenderer final : public Renderer {
|
class TerminalRenderer final : public Renderer {
|
||||||
public:
|
public:
|
||||||
TerminalRenderer();
|
TerminalRenderer();
|
||||||
~TerminalRenderer() override;
|
|
||||||
|
|
||||||
void Draw(Editor &ed) override;
|
~TerminalRenderer() override;
|
||||||
|
|
||||||
|
void Draw(Editor &ed) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // KTE_TERMINAL_RENDERER_H
|
#endif // KTE_TERMINAL_RENDERER_H
|
||||||
|
|||||||
Reference in New Issue
Block a user