Explorar el Código

switching to ncurses

Bernardo Magri hace 1 mes
padre
commit
4f1529bcf9
Se han modificado 3 ficheros con 847 adiciones y 67 borrados
  1. 1 0
      .envrc
  2. 179 67
      src/main.cpp
  3. 667 0
      src/main_ncurses.cpp

+ 1 - 0
.envrc

@@ -0,0 +1 @@
+use nix

+ 179 - 67
src/main.cpp

@@ -6,6 +6,9 @@
 #include <iostream>
 #include <fstream>
 #include <memory>
+#include <chrono>
+#include <termios.h>
+#include <unistd.h>
 
 // Global debug log
 std::ofstream debug_log("lumacs_debug.log");
@@ -109,10 +112,16 @@ private:
 
     enum class Prefix {
         None,
-        Meta,  // Esc pressed
+        Meta,  // Alt/Meta pressed (ESC+key sequence)
         CtrlX  // C-x pressed
     };
 
+    struct PendingPrefix {
+        Prefix type = Prefix::None;
+        std::chrono::steady_clock::time_point timestamp;
+        static constexpr auto TIMEOUT = std::chrono::milliseconds(500);
+    };
+
     std::unique_ptr<EditorCore> core_;
     std::unique_ptr<LuaApi> lua_api_;
     ScreenInteractive* screen_ = nullptr;
@@ -122,7 +131,13 @@ private:
     // Command/Input state
     Mode mode_ = Mode::Normal;
     std::string command_buffer_;
-    Prefix prefix_ = Prefix::None;
+    PendingPrefix pending_prefix_;
+    
+    bool is_prefix_expired() const {
+        if (pending_prefix_.type == Prefix::None) return false;
+        auto now = std::chrono::steady_clock::now();
+        return (now - pending_prefix_.timestamp) > PendingPrefix::TIMEOUT;
+    }
 
     void handle_editor_event(EditorEvent event) {
         if (event == EditorEvent::Quit) {
@@ -231,11 +246,28 @@ private:
     }
 
     bool handle_input(Event event) {
-        // Debug input in hex
+        // Debug input in hex with more details
         std::string input_debug = event.input();
-        debug_log << "Input Event: is_char=" << event.is_character() << " size=" << input_debug.size() << " bytes=[";
+        debug_log << "=== INPUT DEBUG ===" << std::endl;
+        debug_log << "Event type: ";
+        if (event == Event::Escape) debug_log << "Escape";
+        else if (event == Event::Return) debug_log << "Return";
+        else if (event == Event::Tab) debug_log << "Tab";
+        else if (event == Event::Backspace) debug_log << "Backspace";
+        else if (event == Event::Delete) debug_log << "Delete";
+        else if (event == Event::ArrowUp) debug_log << "ArrowUp";
+        else if (event == Event::ArrowDown) debug_log << "ArrowDown";
+        else if (event == Event::ArrowLeft) debug_log << "ArrowLeft";
+        else if (event == Event::ArrowRight) debug_log << "ArrowRight";
+        else debug_log << "Other";
+        debug_log << std::endl;
+        debug_log << "is_character: " << event.is_character() << std::endl;
+        debug_log << "input_size: " << input_debug.size() << std::endl;
+        debug_log << "bytes: [";
         for(unsigned char c : input_debug) debug_log << std::hex << (int)c << " ";
         debug_log << std::dec << "]" << std::endl;
+        debug_log << "chars: \"" << input_debug << "\"" << std::endl;
+        debug_log << "===================" << std::endl;
 
         // Handle Minibuffer Inputs (Command / FindFile)
         if (mode_ == Mode::Command || mode_ == Mode::FindFile) {
@@ -274,85 +306,146 @@ private:
             return true; 
         }
 
+        // Check for expired prefix first
+        if (is_prefix_expired()) {
+            pending_prefix_.type = Prefix::None;
+        }
+
         // === Key Resolution ===
         std::string key_name;
         std::string input_str = event.input();
-
-        // 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]);
+        bool is_meta_sequence = false;
+
+        // 1. Detect Meta sequences (Alt/ESC + key)
+        if (input_str.size() >= 2 && input_str[0] == 27) {
+            // Debug the meta sequence we got
+            debug_log << "Meta sequence detected: size=" << input_str.size() << " bytes=";
+            for (size_t i = 0; i < input_str.size(); ++i) {
+                debug_log << std::hex << (int)(unsigned char)input_str[i] << " ";
+            }
+            debug_log << std::dec << std::endl;
+            
+            // ESC followed by another key - this is a Meta sequence
+            if (input_str.size() == 3 && input_str[1] == '[') {
+                // Meta+Arrow keys: ESC[A, ESC[B, ESC[C, ESC[D  
+                switch (input_str[2]) {
+                    case 'A': key_name = "M-ArrowUp"; break;
+                    case 'B': key_name = "M-ArrowDown"; break;
+                    case 'C': key_name = "M-ArrowRight"; break;
+                    case 'D': key_name = "M-ArrowLeft"; break;
+                    default: 
+                        debug_log << "Unknown ESC[ sequence: " << input_str[2] << std::endl;
+                        break;
+                }
+                is_meta_sequence = true;
+            } else if (input_str.size() >= 6 && input_str.substr(1, 5) == "[1;3") {
+                // Alternative Meta+Arrow format: ESC[1;3A, ESC[1;3B, ESC[1;3C, ESC[1;3D
+                if (input_str.size() == 6) {
+                    switch (input_str[5]) {
+                        case 'A': key_name = "M-ArrowUp"; break;
+                        case 'B': key_name = "M-ArrowDown"; break;
+                        case 'C': key_name = "M-ArrowRight"; break;
+                        case 'D': key_name = "M-ArrowLeft"; break;
+                        default: 
+                            debug_log << "Unknown ESC[1;3 sequence: " << input_str[5] << std::endl;
+                            break;
+                    }
+                    is_meta_sequence = true;
+                }
+            } else if (input_str[1] != '[') {
+                // Regular Meta+char (not arrow keys)
+                key_name = "M-" + std::string(1, input_str[1]);
+                is_meta_sequence = true;
+            }
+            // If it's ESC[ but not a recognized pattern, let it fall through to normal arrow key handling
+        }
+
+        // 2. Basic Key Mapping (if not Meta sequence)  
+        if (!is_meta_sequence) {
+            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)
+                if (!input_str.empty()) {
+                    int code = static_cast<int>(static_cast<unsigned char>(input_str[0]));
+                    debug_log << "Character code analysis: " << code << std::endl;
+                    if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
+                        char letter = 'a' + (code - 1);
+                        key_name = "C-" + std::string(1, letter);
+                        debug_log << "Detected control key: " << key_name << std::endl;
+                    } else if (code >= 32 || code < 0) { // Printable (including extended ASCII/UTF-8 starts)
+                        key_name = input_str; 
+                        debug_log << "Detected printable char: " << key_name << std::endl;
+                    } else {
+                        debug_log << "Unhandled character code: " << code << std::endl;
                     }
-                } else if (code >= 32 || code < 0) { // Printable (including extended ASCII/UTF-8 starts)
-                    key_name = input_str; 
                 }
             }
         }
 
-        // 2. Apply Prefixes
-        if (prefix_ == Prefix::Meta) {
-            prefix_ = Prefix::None;
+        // 3. Apply Pending Prefixes
+        if (pending_prefix_.type == Prefix::Meta && !is_meta_sequence) {
+            pending_prefix_.type = Prefix::None;
             if (key_name.size() == 1) { // Single char
-                 key_name = "M-" + key_name;
+                key_name = "M-" + key_name;
             } else if (key_name == "ArrowUp") key_name = "M-ArrowUp";
-            // ... map other meta combos if needed
+            else if (key_name == "ArrowDown") key_name = "M-ArrowDown";
+            else if (key_name == "ArrowLeft") key_name = "M-ArrowLeft";
+            else if (key_name == "ArrowRight") key_name = "M-ArrowRight";
+            // For other special keys, just prefix with M-
+            else if (!key_name.empty()) key_name = "M-" + key_name;
         } 
-        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
-            }
+        else if (pending_prefix_.type == Prefix::CtrlX) {
+            pending_prefix_.type = Prefix::None;
             key_name = "C-x " + key_name;
         }
 
         // Debug: show what key was resolved
         if (!key_name.empty()) {
-            message_line_ = "Key: " + key_name;
             debug_log << "Resolved Key: " << key_name << std::endl;
+            debug_log << "Pending prefix: " << static_cast<int>(pending_prefix_.type) << std::endl;
         } else {
+            debug_log << "No key resolved for input: size=" << input_str.size() << std::endl;
             return false;
         }
 
         // === 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;
-        }
+        // Handle C-x prefix (but not Escape - let it be a normal key)
         if (key_name == "C-x") {
-            prefix_ = Prefix::CtrlX;
+            pending_prefix_.type = Prefix::CtrlX;
+            pending_prefix_.timestamp = std::chrono::steady_clock::now();
             message_line_ = "C-x-";
+            debug_log << "Set C-x prefix" << std::endl;
+            return true;
+        }
+        
+        // Handle single Escape as Meta prefix only if no other Meta sequence detected
+        if (key_name == "Escape" && !is_meta_sequence && pending_prefix_.type == Prefix::None) {
+            pending_prefix_.type = Prefix::Meta;
+            pending_prefix_.timestamp = std::chrono::steady_clock::now();
+            message_line_ = "M-";
+            debug_log << "Set Meta prefix" << std::endl;
             return true;
         }
 
         // === Execution ===
 
         // 1. Try Lua key binding
+        debug_log << "Trying Lua binding for: " << key_name << std::endl;
         if (lua_api_->execute_key_binding(key_name)) {
-            message_line_ = "Executed: " + key_name;
+            debug_log << "Lua binding executed successfully" << std::endl;
             return true;
         }
+        debug_log << "No Lua binding found, trying C++ fallbacks" << std::endl;
 
         // 2. C++ Fallback Bindings
         
@@ -377,23 +470,23 @@ private:
             return true;
         }
 
-        // C-x Prefix Commands
-        if (key_name == "C-x o") {
-            core_->next_window();
-            return true;
-        }
-        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;
-        }
+        // C-x Prefix Commands (commented out - using Lua bindings instead)
+        // if (key_name == "C-x o") {
+        //     core_->next_window();
+        //     return true;
+        // }
+        // 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();
@@ -668,7 +761,26 @@ private:
 
 };
 
+// Function to configure terminal for raw input
+void configure_terminal_for_control_chars() {
+    struct termios tty;
+    if (tcgetattr(STDIN_FILENO, &tty) == 0) {
+        // Disable flow control (Ctrl+S, Ctrl+Q)
+        tty.c_iflag &= ~(IXON | IXOFF | IXANY);
+        
+        // Apply settings
+        tcsetattr(STDIN_FILENO, TCSANOW, &tty);
+        
+        debug_log << "Terminal configured for control char capture" << std::endl;
+    } else {
+        debug_log << "Failed to configure terminal" << std::endl;
+    }
+}
+
 int main(int argc, char* argv[]) {
+    // Configure terminal to allow more control characters
+    configure_terminal_for_control_chars();
+    
     auto screen = ScreenInteractive::Fullscreen();
     TuiEditor editor;
 

+ 667 - 0
src/main_ncurses.cpp

@@ -0,0 +1,667 @@
+#include "lumacs/editor_core.hpp"
+#include "lumacs/lua_api.hpp"
+#include <ncurses.h>
+#include <iostream>
+#include <fstream>
+#include <memory>
+#include <chrono>
+#include <string>
+#include <sstream>
+
+// Global debug log
+std::ofstream debug_log("lumacs_debug.log");
+
+using namespace lumacs;
+
+/// ncurses-based TUI frontend for Lumacs
+class NcursesEditor {
+public:
+    NcursesEditor() {
+        // Initialize ncurses
+        initscr();
+        cbreak();           // Disable line buffering
+        noecho();           // Don't echo pressed keys
+        keypad(stdscr, TRUE); // Enable special keys
+        raw();              // Enable all control characters
+        timeout(50);        // Set 50ms timeout for getch() to avoid blocking forever
+        
+        // Color support
+        if (has_colors()) {
+            start_color();
+            use_default_colors();
+            
+            // Define color pairs
+            init_pair(1, COLOR_BLUE, -1);    // Keywords
+            init_pair(2, COLOR_GREEN, -1);   // Strings  
+            init_pair(3, COLOR_WHITE, -1);   // Comments (dim)
+            init_pair(4, COLOR_CYAN, -1);    // Functions
+            init_pair(5, COLOR_YELLOW, -1);  // Types
+            init_pair(6, COLOR_MAGENTA, -1); // Numbers/Constants
+            init_pair(7, COLOR_RED, -1);     // Errors
+        }
+        
+        // Get screen dimensions
+        getmaxyx(stdscr, height_, width_);
+        
+        // Initialize Core first
+        core_ = std::make_unique<EditorCore>();
+        
+        // Set initial viewport size (leave room for status and message lines)
+        int content_height = height_ - 2; // -1 for status, -1 for message
+        int content_width = width_ - 6;   // -6 for line numbers (3 digits + " | ")
+        core_->set_viewport_size(content_width, content_height);
+        
+        // Then LuaApi, which depends on Core
+        lua_api_ = std::make_unique<LuaApi>(*core_);
+        
+        // Listen to editor events
+        core_->on_event([this](EditorEvent event) {
+            handle_editor_event(event);
+        });
+        
+        // Load init.lua configuration
+        bool init_loaded = lua_api_->load_init_file();
+        debug_log << "init.lua loading result: " << (init_loaded ? "success" : "failed") << std::endl;
+        
+        // Debug: List loaded key bindings
+        auto bindings = lua_api_->key_bindings();
+        debug_log << "Loaded " << bindings.size() << " key bindings:" << std::endl;
+        for (const auto& [key, func] : bindings) {
+            debug_log << "  - " << key << std::endl;
+        }
+        
+        // Check if specific C-x bindings are loaded
+        debug_log << "Checking specific bindings:" << std::endl;
+        debug_log << "  C-x 2: " << (lua_api_->has_key_binding("C-x 2") ? "found" : "NOT FOUND") << std::endl;
+        debug_log << "  C-x 3: " << (lua_api_->has_key_binding("C-x 3") ? "found" : "NOT FOUND") << std::endl;
+        debug_log << "  C-x 0: " << (lua_api_->has_key_binding("C-x 0") ? "found" : "NOT FOUND") << std::endl;
+        
+        debug_log << "ncurses editor initialized: " << width_ << "x" << height_ << std::endl;
+    }
+    
+    ~NcursesEditor() {
+        // Cleanup ncurses
+        endwin();
+        
+        // Explicitly destroy Core (and its Buffers/Callbacks) BEFORE LuaApi
+        core_.reset();
+        lua_api_.reset();
+    }
+    
+    void load_file(const std::filesystem::path& path) {
+        if (!core_->load_file(path)) {
+            std::cerr << "Failed to load file: " << path << std::endl;
+        }
+    }
+    
+    void run() {
+        should_quit_ = false;
+        
+        // Initial render
+        render();
+        
+        while (!should_quit_) {
+            // Handle screen resize
+            int new_height, new_width;
+            getmaxyx(stdscr, new_height, new_width);
+            if (new_height != height_ || new_width != width_) {
+                height_ = new_height;
+                width_ = new_width;
+                int content_height = height_ - 2;
+                int content_width = width_ - 6;
+                core_->set_viewport_size(content_width, content_height);
+                debug_log << "Screen resized to: " << width_ << "x" << height_ << std::endl;
+                debug_log << "Content area: " << content_width << "x" << content_height << std::endl;
+                render();  // Re-render after resize
+            }
+            
+            // Get input (with timeout)
+            int ch = getch();
+            
+            // Only process input and render if we got actual input (not timeout)
+            if (ch != ERR) {
+                handle_input(ch);
+                render();
+            }
+        }
+    }
+
+private:
+    enum class Mode {
+        Normal,
+        Command,   // Minibuffer entry
+        FindFile   // Find file prompt
+    };
+    
+    std::unique_ptr<EditorCore> core_;
+    std::unique_ptr<LuaApi> lua_api_;
+    bool should_quit_ = false;
+    std::string message_line_;
+    int height_, width_;
+    
+    // Input state
+    Mode mode_ = Mode::Normal;
+    std::string command_buffer_;
+    
+    // Prefix handling
+    bool waiting_for_prefix_ = false;
+    std::string prefix_key_;
+    std::chrono::steady_clock::time_point prefix_time_;
+    static constexpr auto PREFIX_TIMEOUT = std::chrono::milliseconds(1000);
+    
+    void handle_editor_event(EditorEvent event) {
+        if (event == EditorEvent::Quit) {
+            should_quit_ = true;
+        } else if (event == EditorEvent::Message) {
+            message_line_ = core_->last_message();
+        } else if (event == EditorEvent::CommandMode) {
+            mode_ = Mode::Command;
+            command_buffer_.clear();
+        }
+    }
+    
+    /// Convert ncurses key code to our key name format
+    std::string resolve_key(int ch) {
+        debug_log << "=== NCURSES INPUT DEBUG ===" << std::endl;
+        debug_log << "Raw key code: " << ch << " (0x" << std::hex << ch << std::dec << ")" << std::endl;
+        
+        std::string key_name;
+        
+        // Handle special ncurses key codes first
+        if (ch >= KEY_MIN) {
+            switch (ch) {
+                case KEY_UP: key_name = "ArrowUp"; break;
+                case KEY_DOWN: key_name = "ArrowDown"; break;
+                case KEY_LEFT: key_name = "ArrowLeft"; break;
+                case KEY_RIGHT: key_name = "ArrowRight"; break;
+                case KEY_HOME: key_name = "Home"; break;
+                case KEY_END: key_name = "End"; break;
+                case KEY_BACKSPACE: key_name = "Backspace"; break;
+                case KEY_DC: key_name = "Delete"; break;
+                case KEY_ENTER: key_name = "Return"; break;
+                default:
+                    debug_log << "Unknown special key: " << ch << " (ignoring)" << std::endl;
+                    // Return empty string to ignore unknown special keys
+                    return "";
+                    break;
+            }
+        } else {
+            // Handle normal ASCII characters and control codes
+            switch (ch) {
+                case 127: // DEL
+                case 8:   // BS  
+                    key_name = "Backspace"; break;
+                case '\n':
+                case '\r':
+                    key_name = "Return"; break;
+                case '\t': 
+                    key_name = "Tab"; break;
+                case 27: 
+                    key_name = "Escape"; break;
+                default:
+                    // Control characters (1-26, excluding special cases)
+                    if (ch >= 1 && ch <= 26 && ch != 8 && ch != 9 && ch != 10 && ch != 13) {
+                        char letter = 'a' + (ch - 1);
+                        key_name = "C-" + std::string(1, letter);
+                        debug_log << "Control character detected: " << ch << " -> " << key_name << std::endl;
+                    }
+                    // Printable ASCII characters
+                    else if (ch >= 32 && ch <= 126) {
+                        key_name = std::string(1, static_cast<char>(ch));
+                    }
+                    // Extended characters (might be Meta combinations)
+                    else if (ch >= 128 && ch < 256) {
+                        char base_char = ch - 128;
+                        if (base_char >= 32 && base_char <= 126) {
+                            key_name = "M-" + std::string(1, base_char);
+                        }
+                    }
+                    else {
+                        debug_log << "Unhandled character code: " << ch << std::endl;
+                    }
+                    break;
+            }
+        }
+        
+        debug_log << "Resolved key: '" << key_name << "'" << std::endl;
+        debug_log << "============================" << std::endl;
+        
+        return key_name;
+    }
+    
+    bool handle_input(int ch) {
+        // Handle minibuffer/command mode
+        if (mode_ == Mode::Command || mode_ == Mode::FindFile) {
+            if (ch == 27) { // Escape
+                mode_ = Mode::Normal;
+                command_buffer_.clear();
+                message_line_ = "Cancelled";
+                return true;
+            }
+            if (ch == '\n' || ch == '\r') { // Return
+                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;
+            }
+            if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
+                if (!command_buffer_.empty()) {
+                    command_buffer_.pop_back();
+                } else {
+                    mode_ = Mode::Normal;
+                }
+                return true;
+            }
+            if (ch >= 32 && ch <= 126) { // Printable characters
+                command_buffer_ += static_cast<char>(ch);
+                return true;
+            }
+            return true;
+        }
+        
+        // Normal mode - resolve key and try bindings
+        std::string key_name = resolve_key(ch);
+        if (key_name.empty()) {
+            debug_log << "Empty key name, ignoring input" << std::endl;
+            return false;
+        }
+        
+        // Check for expired prefix
+        if (waiting_for_prefix_) {
+            auto now = std::chrono::steady_clock::now();
+            if (now - prefix_time_ > PREFIX_TIMEOUT) {
+                debug_log << "Prefix timeout, clearing" << std::endl;
+                waiting_for_prefix_ = false;
+                prefix_key_.clear();
+                message_line_ = "Prefix timeout";
+            }
+        }
+        
+        // Handle prefix sequences
+        std::string final_key_name = key_name;
+        if (waiting_for_prefix_) {
+            final_key_name = prefix_key_ + " " + key_name;
+            waiting_for_prefix_ = false;
+            prefix_key_.clear();
+            debug_log << "Composite key: " << final_key_name << std::endl;
+        }
+        
+        // Check if this key should start a prefix
+        if (key_name == "C-x" && !waiting_for_prefix_) {
+            waiting_for_prefix_ = true;
+            prefix_key_ = "C-x";
+            prefix_time_ = std::chrono::steady_clock::now();
+            message_line_ = "C-x-";
+            debug_log << "Starting C-x prefix" << std::endl;
+            return true;
+        }
+        
+        // Show what we're trying to bind
+        message_line_ = "Key: " + final_key_name;
+        
+        // Try Lua key binding first
+        debug_log << "Trying Lua binding for: " << final_key_name << std::endl;
+        bool has_lua_binding = lua_api_->has_key_binding(final_key_name);
+        debug_log << "Has Lua binding: " << (has_lua_binding ? "yes" : "no") << std::endl;
+        
+        if (lua_api_->execute_key_binding(final_key_name)) {
+            debug_log << "Lua binding executed successfully" << std::endl;
+            return true;
+        }
+        debug_log << "No Lua binding executed, trying C++ fallbacks" << std::endl;
+        
+        // C++ fallback bindings (using final_key_name for composite keys)
+        
+        // Quit
+        if (final_key_name == "C-q") {
+            core_->request_quit();
+            return true;
+        }
+        
+        // Command mode
+        if (final_key_name == "M-x" || final_key_name == ":") {
+            mode_ = Mode::Command;
+            command_buffer_.clear();
+            return true;
+        }
+        
+        // File operations
+        if (final_key_name == "C-x C-f") {
+            mode_ = Mode::FindFile;
+            command_buffer_.clear();
+            return true;
+        }
+        
+        // Navigation
+        if (final_key_name == "ArrowUp") { 
+            auto before = core_->cursor();
+            core_->move_up(); 
+            auto after = core_->cursor();
+            debug_log << "ArrowUp: cursor moved from (" << before.line << "," << before.column << ") to (" << after.line << "," << after.column << ")" << std::endl;
+            return true; 
+        }
+        if (final_key_name == "ArrowDown") { 
+            auto before = core_->cursor();
+            core_->move_down(); 
+            auto after = core_->cursor();
+            debug_log << "ArrowDown: cursor moved from (" << before.line << "," << before.column << ") to (" << after.line << "," << after.column << ")" << std::endl;
+            return true; 
+        }
+        if (final_key_name == "ArrowLeft") { 
+            auto before = core_->cursor();
+            core_->move_left(); 
+            auto after = core_->cursor();
+            debug_log << "ArrowLeft: cursor moved from (" << before.line << "," << before.column << ") to (" << after.line << "," << after.column << ")" << std::endl;
+            return true; 
+        }
+        if (final_key_name == "ArrowRight") { 
+            auto before = core_->cursor();
+            core_->move_right(); 
+            auto after = core_->cursor();
+            debug_log << "ArrowRight: cursor moved from (" << before.line << "," << before.column << ") to (" << after.line << "," << after.column << ")" << std::endl;
+            return true; 
+        }
+        if (final_key_name == "Home") { core_->move_to_line_start(); return true; }
+        if (final_key_name == "End") { core_->move_to_line_end(); return true; }
+        
+        // Editing
+        if (final_key_name == "Backspace") {
+            auto cursor = core_->cursor();
+            core_->buffer().erase_char(cursor);
+            if (cursor.column > 0) {
+                core_->set_cursor({cursor.line, cursor.column - 1});
+            } else if (cursor.line > 0) {
+                size_t prev_line_len = core_->buffer().line(cursor.line - 1).size();
+                core_->set_cursor({cursor.line - 1, prev_line_len});
+            }
+            return true;
+        }
+        
+        if (final_key_name == "Delete") {
+            auto cursor = core_->cursor();
+            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) {
+                core_->buffer().erase_char({cursor.line + 1, 0});
+            }
+            return true;
+        }
+        
+        if (final_key_name == "Return") {
+            auto cursor = core_->cursor();
+            core_->buffer().insert_newline(cursor);
+            core_->set_cursor({cursor.line + 1, 0});
+            return true;
+        }
+        
+        if (final_key_name == "Tab") {
+            auto cursor = core_->cursor();
+            core_->buffer().insert(cursor, "    ");
+            core_->set_cursor({cursor.line, cursor.column + 4});
+            return true;
+        }
+        
+        // Insert printable characters (use original key_name for this)
+        if (key_name.size() == 1 && key_name[0] >= 32 && key_name[0] <= 126) {
+            auto cursor = core_->cursor();
+            core_->buffer().insert_char(cursor, key_name[0]);
+            core_->set_cursor({cursor.line, cursor.column + 1});
+            return true;
+        }
+        
+        return false;
+    }
+    
+    void execute_command(const std::string& cmd) {
+        if (cmd.empty()) return;
+        
+        std::istringstream iss(cmd);
+        std::string command;
+        iss >> command;
+        
+        if (command == "q" || command == "quit") {
+            core_->request_quit();
+            return;
+        }
+        
+        if (command == "w" || command == "write") {
+            core_->buffer().save();
+            message_line_ = "Saved";
+            return;
+        }
+        
+        if (command == "wq") {
+            core_->buffer().save();
+            core_->request_quit();
+            return;
+        }
+        
+        if (command == "e" || command == "edit") {
+            std::string path;
+            std::getline(iss >> std::ws, path);
+            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";
+        } else {
+            message_line_ = "Unknown command: " + cmd;
+        }
+    }
+    
+    int color_for_attribute(TextAttribute::ColorType color) {
+        switch (color) {
+            case TextAttribute::ColorType::Keyword: return COLOR_PAIR(1);
+            case TextAttribute::ColorType::String: return COLOR_PAIR(2);
+            case TextAttribute::ColorType::Comment: return COLOR_PAIR(3);
+            case TextAttribute::ColorType::Function: return COLOR_PAIR(4);
+            case TextAttribute::ColorType::Type: return COLOR_PAIR(5);
+            case TextAttribute::ColorType::Number:
+            case TextAttribute::ColorType::Constant: return COLOR_PAIR(6);
+            case TextAttribute::ColorType::Error: return COLOR_PAIR(7);
+            default: return 0;
+        }
+    }
+    
+    void render() {
+        // Clear and update screen info
+        clear();
+        getmaxyx(stdscr, height_, width_);
+        
+        // Calculate content area (leave room for status and message lines)
+        int content_height = height_ - 2; 
+        int content_width = width_;
+        
+        // Render the layout tree recursively
+        render_layout_node(core_->root_layout(), 0, 0, content_width, content_height);
+        
+        // Status line (second to last line)
+        render_status_line();
+        
+        // Message/command line (last line)
+        render_message_line();
+        
+        // Refresh screen
+        refresh();
+    }
+    
+    void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height) {
+        if (!node) return;
+        
+        if (node->type == LayoutNode::Type::Leaf) {
+            // Render a single window
+            render_window(node->window, x, y, width, height);
+        } else if (node->type == LayoutNode::Type::HorizontalSplit) {
+            // Split horizontally: top and bottom windows
+            int top_height = height / 2;
+            int bottom_height = height - top_height;
+            
+            render_layout_node(node->child1, x, y, width, top_height);
+            render_layout_node(node->child2, x, y + top_height, width, bottom_height);
+        } else if (node->type == LayoutNode::Type::VerticalSplit) {
+            // Split vertically: left and right windows  
+            int left_width = width / 2;
+            int right_width = width - left_width;
+            
+            render_layout_node(node->child1, x, y, left_width, height);
+            render_layout_node(node->child2, x + left_width, y, right_width, height);
+        }
+    }
+    
+    void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height) {
+        if (!window) return;
+        
+        // Update window viewport size
+        int line_number_width = 6; // "999 │ "
+        int content_width = width - line_number_width;
+        window->set_viewport_size(content_width, height);
+        
+        // Get window data
+        const auto& buffer = window->buffer();
+        const auto cursor = window->cursor();
+        auto [start_line, end_line] = window->visible_line_range();
+        size_t buffer_line_count = buffer.line_count();
+        bool is_active = (window == core_->active_window());
+        
+        debug_log << "Render window at " << x << "," << y << " size " << width << "x" << height 
+                  << " viewport=" << start_line << "-" << end_line 
+                  << " cursor=(" << cursor.line << "," << cursor.column << ")"
+                  << " active=" << is_active << std::endl;
+        
+        // Render buffer lines
+        for (int screen_y = 0; screen_y < height && start_line + screen_y < end_line; ++screen_y) {
+            size_t buffer_line_idx = start_line + screen_y;
+            const auto& line_text = buffer.line(buffer_line_idx);
+            
+            // Clear this line
+            move(y + screen_y, x);
+            for (int i = 0; i < width; ++i) addch(' ');
+            
+            // Line number
+            mvprintw(y + screen_y, x, "%3zu │ ", buffer_line_idx + 1);
+            
+            // Line content
+            if (!line_text.empty()) {
+                std::string display_text = line_text;
+                int max_content_width = content_width - 1;
+                if ((int)display_text.length() > max_content_width) {
+                    display_text = display_text.substr(0, max_content_width - 3) + "...";
+                }
+                mvprintw(y + screen_y, x + line_number_width, "%s", display_text.c_str());
+            }
+            
+            // Show cursor if this is the cursor line and this is the active window
+            if (buffer_line_idx == cursor.line && is_active && mode_ == Mode::Normal) {
+                int cursor_screen_x = x + line_number_width + cursor.column;
+                if (cursor_screen_x < x + width) {
+                    char cursor_char = ' ';
+                    if (cursor.column < line_text.size()) {
+                        cursor_char = line_text[cursor.column];
+                    }
+                    attron(A_REVERSE);
+                    mvaddch(y + screen_y, cursor_screen_x, cursor_char);
+                    attroff(A_REVERSE);
+                }
+            }
+        }
+        
+        // Fill remaining lines with tildes (for empty lines below buffer)
+        size_t displayed_lines = std::min((size_t)height, end_line - start_line);
+        for (int screen_y = displayed_lines; screen_y < height; ++screen_y) {
+            move(y + screen_y, x);
+            for (int i = 0; i < width; ++i) addch(' ');
+            
+            if (start_line + screen_y >= buffer_line_count) {
+                mvprintw(y + screen_y, x, "~");
+            }
+        }
+        
+        // Draw window border if there are multiple windows
+        if (is_active && core_->root_layout()->type != LayoutNode::Type::Leaf) {
+            // Draw a simple border around the active window
+            attron(A_BOLD);
+            // Top border
+            move(y, x);
+            for (int i = 0; i < width; ++i) addch('-');
+            // Bottom border  
+            if (y + height < height_ - 2) {
+                move(y + height, x);
+                for (int i = 0; i < width; ++i) addch('-');
+            }
+            attroff(A_BOLD);
+        }
+    }
+    
+    void render_status_line() {
+        const auto cursor = core_->cursor();
+        const auto& buffer = core_->buffer();
+        
+        int status_y = height_ - 2;
+        attron(A_REVERSE);
+        move(status_y, 0);
+        clrtoeol();
+        
+        std::string status = buffer.name();
+        if (buffer.is_modified()) status += " [+]";
+        status += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1);
+        status += " | " + std::to_string(width_) + "x" + std::to_string(height_);
+        if (mode_ == Mode::Command) status += " [CMD]";
+        else if (mode_ == Mode::FindFile) status += " [FILE]";
+        
+        mvprintw(status_y, 0, "%s", status.c_str());
+        attroff(A_REVERSE);
+    }
+    
+    void render_message_line() {
+        int msg_y = height_ - 1;
+        move(msg_y, 0);
+        clrtoeol();
+        
+        if (mode_ == Mode::Command) {
+            mvprintw(msg_y, 0, ":%s", command_buffer_.c_str());
+        } else if (mode_ == Mode::FindFile) {
+            mvprintw(msg_y, 0, "Find file: %s", command_buffer_.c_str());
+        } else if (!message_line_.empty()) {
+            mvprintw(msg_y, 0, "%s", message_line_.c_str());
+        }
+    }
+};
+
+int main(int argc, char* argv[]) {
+    try {
+        NcursesEditor editor;
+        
+        // Load file if provided
+        if (argc > 1) {
+            editor.load_file(argv[1]);
+        }
+        
+        
+        editor.run();
+        
+        std::cout << "Goodbye!" << std::endl;
+    } catch (const std::exception& e) {
+        std::cerr << "Error: " << e.what() << std::endl;
+        return 1;
+    }
+    
+    return 0;
+}