|
|
@@ -103,7 +103,14 @@ public:
|
|
|
private:
|
|
|
enum class Mode {
|
|
|
Normal,
|
|
|
- Command // Minibuffer entry
|
|
|
+ Command, // Minibuffer entry (:)
|
|
|
+ FindFile // Find file prompt (C-x C-f)
|
|
|
+ };
|
|
|
+
|
|
|
+ enum class Prefix {
|
|
|
+ None,
|
|
|
+ Meta, // Esc pressed
|
|
|
+ CtrlX // C-x pressed
|
|
|
};
|
|
|
|
|
|
std::unique_ptr<EditorCore> core_;
|
|
|
@@ -112,10 +119,10 @@ private:
|
|
|
bool should_quit_ = false;
|
|
|
std::string message_line_; // For displaying messages in the UI
|
|
|
|
|
|
- // Command mode state
|
|
|
+ // Command/Input state
|
|
|
Mode mode_ = Mode::Normal;
|
|
|
std::string command_buffer_;
|
|
|
- bool meta_pending_ = false; // Emacs-style ESC prefix support
|
|
|
+ Prefix prefix_ = Prefix::None;
|
|
|
|
|
|
void handle_editor_event(EditorEvent event) {
|
|
|
if (event == EditorEvent::Quit) {
|
|
|
@@ -125,11 +132,12 @@ private:
|
|
|
}
|
|
|
} else if (event == EditorEvent::Message) {
|
|
|
message_line_ = core_->last_message();
|
|
|
- // Trigger re-render? The main loop usually handles it if it came from input.
|
|
|
- // But if it came from elsewhere, we might need screen_->Post(Event::Custom)
|
|
|
} else if (event == EditorEvent::CommandMode) {
|
|
|
mode_ = Mode::Command;
|
|
|
command_buffer_.clear();
|
|
|
+ } else if (event == EditorEvent::WindowFocused) {
|
|
|
+ // No specific action needed here, just re-render handled by main loop.
|
|
|
+ // But it's good to have a dedicated event for this.
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -229,8 +237,8 @@ private:
|
|
|
for(unsigned char c : input_debug) debug_log << std::hex << (int)c << " ";
|
|
|
debug_log << std::dec << "]" << std::endl;
|
|
|
|
|
|
- // Handle Command Mode Input
|
|
|
- if (mode_ == Mode::Command) {
|
|
|
+ // Handle Minibuffer Inputs (Command / FindFile)
|
|
|
+ if (mode_ == Mode::Command || mode_ == Mode::FindFile) {
|
|
|
if (event == Event::Escape) {
|
|
|
mode_ = Mode::Normal;
|
|
|
command_buffer_.clear();
|
|
|
@@ -238,7 +246,15 @@ private:
|
|
|
return true;
|
|
|
}
|
|
|
if (event == Event::Return) {
|
|
|
- execute_command(command_buffer_);
|
|
|
+ if (mode_ == Mode::Command) {
|
|
|
+ execute_command(command_buffer_);
|
|
|
+ } else if (mode_ == Mode::FindFile) {
|
|
|
+ if (core_->load_file(command_buffer_)) {
|
|
|
+ message_line_ = "Loaded: " + command_buffer_;
|
|
|
+ } else {
|
|
|
+ message_line_ = "Failed to load: " + command_buffer_;
|
|
|
+ }
|
|
|
+ }
|
|
|
mode_ = Mode::Normal;
|
|
|
command_buffer_.clear();
|
|
|
return true;
|
|
|
@@ -247,7 +263,6 @@ private:
|
|
|
if (!command_buffer_.empty()) {
|
|
|
command_buffer_.pop_back();
|
|
|
} else {
|
|
|
- // Backspace on empty buffer exits command mode
|
|
|
mode_ = Mode::Normal;
|
|
|
}
|
|
|
return true;
|
|
|
@@ -256,202 +271,183 @@ private:
|
|
|
command_buffer_ += event.input();
|
|
|
return true;
|
|
|
}
|
|
|
- // Allow arrows to fall through to default navigation or specific handling if we added history?
|
|
|
- // For now, just consume
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Convert FTXUI event to string key for Lua bindings
|
|
|
+ // === Key Resolution ===
|
|
|
std::string key_name;
|
|
|
std::string input_str = event.input();
|
|
|
|
|
|
- // 1. Handle Meta Pending state (previous key was ESC)
|
|
|
- if (meta_pending_) {
|
|
|
- meta_pending_ = false; // One-shot prefix
|
|
|
-
|
|
|
- if (event == Event::Escape) {
|
|
|
- // ESC ESC -> treat as single Escape (Cancel/Quit)
|
|
|
- key_name = "Escape";
|
|
|
- } else if (event.is_character()) {
|
|
|
- key_name = "M-" + event.input();
|
|
|
- } else if (event == Event::ArrowUp) key_name = "M-ArrowUp";
|
|
|
- else if (event == Event::ArrowDown) key_name = "M-ArrowDown";
|
|
|
- else if (event == Event::ArrowLeft) key_name = "M-ArrowLeft";
|
|
|
- else if (event == Event::ArrowRight) key_name = "M-ArrowRight";
|
|
|
- else {
|
|
|
- // Unknown special key with meta... ignore or try best effort?
|
|
|
- // Fallback to normal processing effectively ignoring the previous ESC
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 2. Check for Raw Meta sequences (Alt+Key sending \x1b + char in one event)
|
|
|
- if (key_name.empty()) {
|
|
|
- if (input_str.size() >= 2 && input_str[0] == 27 && input_str[1] >= 32 && input_str[1] < 127) {
|
|
|
- // Atomic Alt+Char
|
|
|
- key_name = "M-" + std::string(1, input_str[1]);
|
|
|
- } else if (input_str.size() >= 6 && input_str.substr(0,5) == "\x1b[1;3") {
|
|
|
- // Atomic Alt+Arrow (simplified check)
|
|
|
- if (event == Event::ArrowUp) key_name = "M-ArrowUp";
|
|
|
- if (event == Event::ArrowDown) key_name = "M-ArrowDown";
|
|
|
- if (event == Event::ArrowRight) key_name = "M-ArrowRight";
|
|
|
- if (event == Event::ArrowLeft) key_name = "M-ArrowLeft";
|
|
|
+ // 1. Basic Key Mapping
|
|
|
+ if (event == Event::Escape) key_name = "Escape";
|
|
|
+ else if (event == Event::ArrowUp) key_name = "ArrowUp";
|
|
|
+ else if (event == Event::ArrowDown) key_name = "ArrowDown";
|
|
|
+ else if (event == Event::ArrowLeft) key_name = "ArrowLeft";
|
|
|
+ else if (event == Event::ArrowRight) key_name = "ArrowRight";
|
|
|
+ else if (event == Event::Home) key_name = "Home";
|
|
|
+ else if (event == Event::End) key_name = "End";
|
|
|
+ else if (event == Event::Return) key_name = "Return";
|
|
|
+ else if (event == Event::Tab) key_name = "Tab";
|
|
|
+ else if (event == Event::Backspace) key_name = "Backspace";
|
|
|
+ else if (event == Event::Delete) key_name = "Delete";
|
|
|
+ else {
|
|
|
+ // Check based on input content (handles Chars, Control keys, Meta)
|
|
|
+ if (!input_str.empty()) {
|
|
|
+ int code = static_cast<int>(static_cast<unsigned char>(input_str[0]));
|
|
|
+ if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
|
|
|
+ char letter = 'a' + (code - 1);
|
|
|
+ key_name = "C-" + std::string(1, letter);
|
|
|
+ } else if (input_str.size() >= 2 && input_str[0] == 27) {
|
|
|
+ // Alt+Char (Meta) - often sent as ESC + char
|
|
|
+ // Avoid parsing arrow keys (ESC [ ...) as Meta
|
|
|
+ if (input_str[1] != '[') {
|
|
|
+ key_name = "M-" + std::string(1, input_str[1]);
|
|
|
+ }
|
|
|
+ } else if (code >= 32 || code < 0) { // Printable (including extended ASCII/UTF-8 starts)
|
|
|
+ key_name = input_str;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 3. Handle standard Control/Special keys if not yet resolved
|
|
|
- if (key_name.empty()) {
|
|
|
- if (event == Event::Escape) {
|
|
|
- // Don't act immediately, set pending
|
|
|
- meta_pending_ = true;
|
|
|
- message_line_ = "M- (Press key)";
|
|
|
- return true;
|
|
|
- } else if (event == Event::ArrowUp) {
|
|
|
- key_name = "ArrowUp";
|
|
|
- } else if (event == Event::ArrowDown) {
|
|
|
- key_name = "ArrowDown";
|
|
|
- }
|
|
|
- else if (event == Event::ArrowLeft) {
|
|
|
- key_name = "ArrowLeft";
|
|
|
- } else if (event == Event::ArrowRight) {
|
|
|
- key_name = "ArrowRight";
|
|
|
- } else if (event == Event::Home) {
|
|
|
- key_name = "Home";
|
|
|
- } else if (event == Event::End) {
|
|
|
- key_name = "End";
|
|
|
- } else if (event == Event::Return) {
|
|
|
- key_name = "Return";
|
|
|
- } else if (event == Event::Tab) {
|
|
|
- key_name = "Tab";
|
|
|
- } else if (event.is_character()) {
|
|
|
- // Check control chars
|
|
|
- if (!input_str.empty()) {
|
|
|
- int code = static_cast<int>(static_cast<unsigned char>(input_str[0]));
|
|
|
- if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
|
|
|
- char letter = 'a' + (code - 1);
|
|
|
- key_name = "C-" + std::string(1, letter);
|
|
|
- } else {
|
|
|
- key_name = input_str;
|
|
|
- }
|
|
|
- }
|
|
|
+ // 2. Apply Prefixes
|
|
|
+ if (prefix_ == Prefix::Meta) {
|
|
|
+ prefix_ = Prefix::None;
|
|
|
+ if (key_name.size() == 1) { // Single char
|
|
|
+ key_name = "M-" + key_name;
|
|
|
+ } else if (key_name == "ArrowUp") key_name = "M-ArrowUp";
|
|
|
+ // ... map other meta combos if needed
|
|
|
+ }
|
|
|
+ else if (prefix_ == Prefix::CtrlX) {
|
|
|
+ prefix_ = Prefix::None;
|
|
|
+ if (key_name == "C-f") { // Handle C-f specially if needed or let it be C-x C-f
|
|
|
+ // C-x C-f is standard
|
|
|
}
|
|
|
+ key_name = "C-x " + key_name;
|
|
|
}
|
|
|
|
|
|
- // Debug: show what key was pressed
|
|
|
+ // Debug: show what key was resolved
|
|
|
if (!key_name.empty()) {
|
|
|
message_line_ = "Key: " + key_name;
|
|
|
debug_log << "Resolved Key: " << key_name << std::endl;
|
|
|
+ } else {
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
- // Try Lua key binding first
|
|
|
- if (!key_name.empty() && lua_api_->execute_key_binding(key_name)) {
|
|
|
- message_line_ = "Executed: " + key_name;
|
|
|
+ // === Prefix Trigger Handling ===
|
|
|
+ // If resolved key is "Escape" or "C-x", set prefix and return
|
|
|
+ if (key_name == "Escape") {
|
|
|
+ prefix_ = Prefix::Meta;
|
|
|
+ message_line_ = "M-";
|
|
|
return true;
|
|
|
}
|
|
|
-
|
|
|
- // Fallback to default bindings
|
|
|
-
|
|
|
- // Quit commands (Explicit Q binding if generic quit failed)
|
|
|
- // Note: Escape is now handled by meta_pending logic. To quit via Escape, press ESC ESC.
|
|
|
- // Or we can check if generic "Escape" key_name was produced (from double esc).
|
|
|
- if (key_name == "Escape") {
|
|
|
- core_->request_quit();
|
|
|
- return true;
|
|
|
+ if (key_name == "C-x") {
|
|
|
+ prefix_ = Prefix::CtrlX;
|
|
|
+ message_line_ = "C-x-";
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- // Ctrl+C to quit
|
|
|
- if (event.is_character()) {
|
|
|
- auto input = event.input();
|
|
|
- if (!input.empty() && input[0] == 3) { // Ctrl+C
|
|
|
- core_->request_quit();
|
|
|
- return true;
|
|
|
- }
|
|
|
+ // === Execution ===
|
|
|
+
|
|
|
+ // 1. Try Lua key binding
|
|
|
+ if (lua_api_->execute_key_binding(key_name)) {
|
|
|
+ message_line_ = "Executed: " + key_name;
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- // Arrow keys for cursor movement
|
|
|
- if (event == Event::ArrowUp) {
|
|
|
- core_->move_up();
|
|
|
+ // 2. C++ Fallback Bindings
|
|
|
+
|
|
|
+ // Global
|
|
|
+ if (key_name == "C-q") {
|
|
|
+ core_->request_quit();
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::ArrowDown) {
|
|
|
- core_->move_down();
|
|
|
+ if (key_name == "C-s") {
|
|
|
+ core_->buffer().save();
|
|
|
+ message_line_ = "Saved";
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::ArrowLeft) {
|
|
|
- core_->move_left();
|
|
|
+ if (key_name == "M-x") {
|
|
|
+ mode_ = Mode::Command;
|
|
|
+ message_line_ = ":";
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::ArrowRight) {
|
|
|
- core_->move_right();
|
|
|
+ if (key_name == ":") { // Vim style
|
|
|
+ mode_ = Mode::Command;
|
|
|
+ message_line_ = ":";
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Home/End
|
|
|
- if (event == Event::Home) {
|
|
|
- core_->move_to_line_start();
|
|
|
+ // C-x Prefix Commands
|
|
|
+ if (key_name == "C-x o") {
|
|
|
+ core_->next_window();
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::End) {
|
|
|
- core_->move_to_line_end();
|
|
|
+ if (key_name == "C-x 2") {
|
|
|
+ core_->split_horizontally();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (key_name == "C-x 3") {
|
|
|
+ core_->split_vertically();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (key_name == "C-x 0") {
|
|
|
+ core_->close_active_window();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (key_name == "C-x C-f") {
|
|
|
+ mode_ = Mode::FindFile;
|
|
|
+ command_buffer_.clear();
|
|
|
+ message_line_ = "Find file: ";
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Backspace
|
|
|
- if (event == Event::Backspace) {
|
|
|
+ // Editing / Navigation
|
|
|
+ if (key_name == "ArrowUp") { core_->move_up(); return true; }
|
|
|
+ if (key_name == "ArrowDown") { core_->move_down(); return true; }
|
|
|
+ if (key_name == "ArrowLeft") { core_->move_left(); return true; }
|
|
|
+ if (key_name == "ArrowRight") { core_->move_right(); return true; }
|
|
|
+ if (key_name == "Home") { core_->move_to_line_start(); return true; }
|
|
|
+ if (key_name == "End") { core_->move_to_line_end(); return true; }
|
|
|
+
|
|
|
+ if (key_name == "Backspace") {
|
|
|
auto cursor = core_->cursor();
|
|
|
core_->buffer().erase_char(cursor);
|
|
|
- // Move cursor back if we deleted a character
|
|
|
if (cursor.column > 0) {
|
|
|
core_->set_cursor({cursor.line, cursor.column - 1});
|
|
|
} else if (cursor.line > 0) {
|
|
|
- // Moved to end of previous line
|
|
|
size_t prev_line_len = core_->buffer().line(cursor.line - 1).size();
|
|
|
core_->set_cursor({cursor.line - 1, prev_line_len});
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
-
|
|
|
- // Delete key (delete character at cursor)
|
|
|
- if (event == Event::Delete) {
|
|
|
+ if (key_name == "Delete") {
|
|
|
auto cursor = core_->cursor();
|
|
|
- // Delete the character after cursor by erasing at cursor+1
|
|
|
if (cursor.column < core_->buffer().line(cursor.line).size()) {
|
|
|
core_->buffer().erase_char({cursor.line, cursor.column + 1});
|
|
|
} else if (cursor.line < core_->buffer().line_count() - 1) {
|
|
|
- // At end of line, join with next line
|
|
|
core_->buffer().erase_char({cursor.line + 1, 0});
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
-
|
|
|
- // Return/Enter - insert newline
|
|
|
- if (event == Event::Return) {
|
|
|
+ if (key_name == "Return") {
|
|
|
auto cursor = core_->cursor();
|
|
|
core_->buffer().insert_newline(cursor);
|
|
|
- // Move cursor to start of next line
|
|
|
core_->set_cursor({cursor.line + 1, 0});
|
|
|
return true;
|
|
|
}
|
|
|
-
|
|
|
- // Tab - insert tab character or spaces
|
|
|
- if (event == Event::Tab) {
|
|
|
+ if (key_name == "Tab") {
|
|
|
auto cursor = core_->cursor();
|
|
|
- // Insert 4 spaces (or you could insert '\t')
|
|
|
core_->buffer().insert(cursor, " ");
|
|
|
core_->set_cursor({cursor.line, cursor.column + 4});
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Regular printable characters
|
|
|
- if (event.is_character()) {
|
|
|
- std::string input = event.input();
|
|
|
- if (!input.empty() && input[0] >= 32 && input[0] < 127) {
|
|
|
- // Printable ASCII character
|
|
|
- auto cursor = core_->cursor();
|
|
|
- core_->buffer().insert_char(cursor, input[0]);
|
|
|
- // Move cursor forward
|
|
|
- core_->set_cursor({cursor.line, cursor.column + 1});
|
|
|
- return true;
|
|
|
- }
|
|
|
+ // Insert printable characters
|
|
|
+ if (key_name.size() == 1) {
|
|
|
+ auto cursor = core_->cursor();
|
|
|
+ core_->buffer().insert_char(cursor, key_name[0]);
|
|
|
+ core_->set_cursor({cursor.line, cursor.column + 1});
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
@@ -460,23 +456,43 @@ private:
|
|
|
void execute_command(const std::string& cmd) {
|
|
|
if (cmd.empty()) return;
|
|
|
|
|
|
- if (cmd == "q" || cmd == "quit") {
|
|
|
+ // Simple command parsing (first word is command, rest is args)
|
|
|
+ std::istringstream iss(cmd);
|
|
|
+ std::string command;
|
|
|
+ iss >> command;
|
|
|
+
|
|
|
+ if (command == "q" || command == "quit") {
|
|
|
core_->request_quit();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if (cmd == "w" || cmd == "write") {
|
|
|
+ if (command == "w" || command == "write") {
|
|
|
core_->buffer().save();
|
|
|
message_line_ = "Saved";
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if (cmd == "wq") {
|
|
|
+ if (command == "wq") {
|
|
|
core_->buffer().save();
|
|
|
core_->request_quit();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ if (command == "e" || command == "edit") {
|
|
|
+ std::string path;
|
|
|
+ std::getline(iss >> std::ws, path); // Read rest of line
|
|
|
+ if (!path.empty()) {
|
|
|
+ if (core_->load_file(path)) {
|
|
|
+ message_line_ = "Loaded: " + path;
|
|
|
+ } else {
|
|
|
+ message_line_ = "Failed to load: " + path;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ message_line_ = "Usage: :e <filename>";
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// Try executing as Lua
|
|
|
if (lua_api_->execute(cmd)) {
|
|
|
message_line_ = "Lua Executed";
|
|
|
@@ -537,10 +553,16 @@ private:
|
|
|
auto status_elem = text(status) | inverted;
|
|
|
if (!is_active) status_elem = status_elem | dim;
|
|
|
|
|
|
- return vbox({
|
|
|
+ auto window_content = vbox({
|
|
|
vbox(lines) | flex,
|
|
|
status_elem
|
|
|
- }) | border | size(WIDTH, EQUAL, width) | size(HEIGHT, EQUAL, height);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (is_active) {
|
|
|
+ return window_content | border | color(Color::Cyan) | bold | size(WIDTH, EQUAL, width) | size(HEIGHT, EQUAL, height);
|
|
|
+ } else {
|
|
|
+ return window_content | border | size(WIDTH, EQUAL, width) | size(HEIGHT, EQUAL, height);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
void update_layout_sizes(LayoutNode* node, int w, int h) {
|
|
|
@@ -619,6 +641,13 @@ private:
|
|
|
text(command_buffer_),
|
|
|
text(" ") | inverted // Fake cursor
|
|
|
});
|
|
|
+ } else if (mode_ == Mode::FindFile) {
|
|
|
+ debug_log << " Mode: FindFile" << std::endl;
|
|
|
+ bottom_bar = hbox({
|
|
|
+ text("Find file: ") | bold | color(Color::Cyan),
|
|
|
+ text(command_buffer_),
|
|
|
+ text(" ") | inverted // Fake cursor
|
|
|
+ });
|
|
|
} else {
|
|
|
debug_log << " Mode: Normal" << std::endl;
|
|
|
if (!message_line_.empty()) {
|