|
|
@@ -60,7 +60,329 @@ private:
|
|
|
};
|
|
|
|
|
|
// --- TuiEditor Public Method Definitions ---
|
|
|
-// ... (init, run, handle_editor_event, set_core methods unchanged) ...
|
|
|
+
|
|
|
+void TuiEditor::init() {
|
|
|
+ // 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get screen dimensions
|
|
|
+ getmaxyx(stdscr, height_, width_);
|
|
|
+
|
|
|
+ // Initialize theme colors for ncurses
|
|
|
+ if (has_colors() && core_->theme_manager().active_theme()) {
|
|
|
+ core_->theme_manager().active_theme()->initialize_ncurses_colors();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set initial viewport size (leave room for minibuffer/message line)
|
|
|
+ int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
|
|
|
+ int content_height = height_ - minibuffer_lines;
|
|
|
+ bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
|
|
|
+ int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
|
|
|
+ int content_width = width_ - line_number_width;
|
|
|
+ core_->set_viewport_size(content_width, content_height);
|
|
|
+
|
|
|
+ debug_log << "ncurses editor initialized: " << width_ << "x" << height_ << std::endl;
|
|
|
+}
|
|
|
+
|
|
|
+void TuiEditor::run() {
|
|
|
+ should_quit_ = false;
|
|
|
+
|
|
|
+ // Initial render
|
|
|
+ render();
|
|
|
+
|
|
|
+ std::chrono::steady_clock::time_point last_blink_toggle_time = std::chrono::steady_clock::now();
|
|
|
+
|
|
|
+ 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 minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
|
|
|
+ int content_height = height_ - minibuffer_lines;
|
|
|
+ bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
|
|
|
+ int line_number_width = show_line_numbers ? core_->config().get<int>("line_number_width", 6) : 0;
|
|
|
+ int content_width = width_ - line_number_width;
|
|
|
+ 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;
|
|
|
+ // Force cursor to be visible after resize, as it implies movement.
|
|
|
+ last_cursor_move_time_ = std::chrono::steady_clock::now();
|
|
|
+ cursor_visible_ = true;
|
|
|
+ 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) {
|
|
|
+ last_cursor_move_time_ = std::chrono::steady_clock::now();
|
|
|
+ cursor_visible_ = true; // Ensure cursor is visible on input
|
|
|
+ handle_input(ch);
|
|
|
+ render();
|
|
|
+ last_blink_toggle_time = std::chrono::steady_clock::now(); // Reset blink timer after input
|
|
|
+ } else { // No input (timeout occurred)
|
|
|
+ auto now = std::chrono::steady_clock::now();
|
|
|
+ if (now - last_cursor_move_time_ > BLINK_STATIONARY_THRESHOLD) {
|
|
|
+ // If stationary for long enough, start/continue blinking
|
|
|
+ if (now - last_blink_toggle_time > BLINK_INTERVAL) {
|
|
|
+ cursor_visible_ = !cursor_visible_;
|
|
|
+ core_->check_and_clear_message(); // Check and clear messages (like for GtkEditor's blink)
|
|
|
+ render(); // Re-render to show/hide cursor
|
|
|
+ last_blink_toggle_time = now;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Still "moving" recently, keep cursor visible.
|
|
|
+ // If it was just hidden by a blink, make it visible again.
|
|
|
+ if (!cursor_visible_) {
|
|
|
+ cursor_visible_ = true;
|
|
|
+ render(); // Make sure it's visible
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void TuiEditor::handle_editor_event(EditorEvent event) {
|
|
|
+ if (event == EditorEvent::Quit) {
|
|
|
+ should_quit_ = true;
|
|
|
+ } else if (event == EditorEvent::Message) {
|
|
|
+ message_line_ = core_->last_message(); // Still update local message_line_ for rendering
|
|
|
+ } else if (event == EditorEvent::CursorMoved) {
|
|
|
+ last_cursor_move_time_ = std::chrono::steady_clock::now();
|
|
|
+ cursor_visible_ = true;
|
|
|
+ render(); // Ensure immediate redraw to show cursor at new position
|
|
|
+ } else if (event == EditorEvent::TransientMessageCleared) {
|
|
|
+ // Redraw to clear the message from the screen
|
|
|
+ render();
|
|
|
+ } else if (event == EditorEvent::CommandMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::Command, ":",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ if (input == "q" || input == "quit") {
|
|
|
+ should_quit_ = true;
|
|
|
+ } else {
|
|
|
+ auto result = core_->minibuffer_manager().parse_and_execute_command_string(input);
|
|
|
+ core_->set_message(result.message);
|
|
|
+ }
|
|
|
+ }, // Added comma here
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
+ );
|
|
|
+ } else if (event == EditorEvent::BufferSwitchMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::BufferName, "Switch to buffer: ",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ if (core_->switch_buffer_in_window(input)) {
|
|
|
+ core_->set_message("Switched to: " + input);
|
|
|
+ core_->lua_api()->execute("auto_activate_major_mode()");
|
|
|
+ } else {
|
|
|
+ core_->set_message("Buffer not found: " + input);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
+ );
|
|
|
+ } else if (event == EditorEvent::KillBufferMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::BufferName, "Kill buffer: ",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ // MinibufferManager should handle confirmation for modified buffers
|
|
|
+ if (core_->close_buffer(input)) {
|
|
|
+ core_->set_message("Closed buffer: " + input);
|
|
|
+ } else {
|
|
|
+ core_->set_message("Failed to close buffer: " + input);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
+ );
|
|
|
+ } else if (event == EditorEvent::FindFileMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::FilePath, "Find file: ",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ if (core_->load_file(input)) {
|
|
|
+ core_->set_message("Loaded: " + input);
|
|
|
+ core_->lua_api()->execute("auto_activate_major_mode()");
|
|
|
+ } else {
|
|
|
+ core_->set_message("Failed to load: " + input);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
+ );
|
|
|
+ } else if (event == EditorEvent::ThemeSelectionMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::ThemeName, "Set theme: ",
|
|
|
+ [this](const std::string& input) {
|
|
|
+ auto theme_names = core_->theme_manager().theme_names();
|
|
|
+ auto it = std::find(theme_names.begin(), theme_names.end(), input);
|
|
|
+ if (it != theme_names.end()) {
|
|
|
+ core_->theme_manager().set_active_theme(input);
|
|
|
+ core_->set_message("Switched to theme: " + input);
|
|
|
+ } else {
|
|
|
+ core_->set_message("Theme not found: " + input);
|
|
|
+ }
|
|
|
+ }, // Added comma here
|
|
|
+ [this]() { core_->set_message("Cancelled"); }
|
|
|
+ );
|
|
|
+ } else if (event == EditorEvent::ISearchMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::ISearch, "I-search: ",
|
|
|
+ [](const std::string&) { },
|
|
|
+ [this]() { core_->set_message("Cancelled I-search"); }
|
|
|
+ );
|
|
|
+ core_->minibuffer_manager().start_isearch(true);
|
|
|
+ } else if (event == EditorEvent::ISearchBackwardMode) {
|
|
|
+ core_->minibuffer_manager().activate_minibuffer(
|
|
|
+ MinibufferMode::ISearch, "I-search backward: ",
|
|
|
+ [](const std::string&) { },
|
|
|
+ [this]() { core_->set_message("Cancelled I-search backward"); }
|
|
|
+ );
|
|
|
+ core_->minibuffer_manager().start_isearch(false);
|
|
|
+ } else if (event == EditorEvent::TransientMessageCleared) {
|
|
|
+ // Redraw to clear the message from the screen
|
|
|
+ render();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void TuiEditor::set_core(EditorCore* core) {
|
|
|
+ core_ = core;
|
|
|
+}
|
|
|
+
|
|
|
+// --- TuiEditor Private Helper Method Definitions ---
|
|
|
+
|
|
|
+int TuiEditor::get_attributes_for_face(const std::string& face_name) {
|
|
|
+ auto theme = core_->theme_manager().active_theme();
|
|
|
+ if (theme) {
|
|
|
+ return theme->get_face_attributes_ncurses(face_name);
|
|
|
+ }
|
|
|
+ return 0; // A_NORMAL
|
|
|
+}
|
|
|
+
|
|
|
+/// Convert ncurses key code to our key name format
|
|
|
+std::string TuiEditor::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;
|
|
|
+
|
|
|
+ if (ch == 27) return "Escape";
|
|
|
+ if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) return "Return";
|
|
|
+ if (ch == '\t') return "Tab";
|
|
|
+ if (ch == KEY_BACKSPACE || ch == 127 || ch == '\b') return "Backspace";
|
|
|
+ if (ch == KEY_UP) return "ArrowUp";
|
|
|
+ if (ch == KEY_DOWN) return "ArrowDown";
|
|
|
+ if (ch == KEY_LEFT) return "ArrowLeft";
|
|
|
+ if (ch == KEY_RIGHT) return "ArrowRight";
|
|
|
+ if (ch == KEY_HOME) return "Home";
|
|
|
+ if (ch == KEY_END) return "End";
|
|
|
+ if (ch == KEY_PPAGE) return "PageUp";
|
|
|
+ if (ch == KEY_NPAGE) return "PageDown";
|
|
|
+ if (ch == ' ') return "Space";
|
|
|
+
|
|
|
+ // Control keys
|
|
|
+ if (ch > 0 && ch < 32) {
|
|
|
+ return "C-" + std::string(1, (char)('a' + ch - 1));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Regular characters
|
|
|
+ if (ch >= 32 && ch < 127) {
|
|
|
+ return std::string(1, (char)ch);
|
|
|
+ }
|
|
|
+
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
+bool TuiEditor::handle_input(int ch) {
|
|
|
+ std::string key_name;
|
|
|
+
|
|
|
+ // Check for Meta key sequence (Escape + Key)
|
|
|
+ if (ch == 27) {
|
|
|
+ // Set non-blocking read to check for immediate next key
|
|
|
+ timeout(0);
|
|
|
+ int next_ch = getch();
|
|
|
+ timeout(50); // Restore timeout
|
|
|
+
|
|
|
+ if (next_ch != ERR) {
|
|
|
+ // Ensure next_ch is a valid printable char or special key we can map
|
|
|
+ // For now, handle simple M-char sequences
|
|
|
+ std::string next_key = resolve_key(next_ch);
|
|
|
+ if (!next_key.empty() && next_key.length() == 1) { // Simple char
|
|
|
+ key_name = "M-" + next_key;
|
|
|
+ } else if (!next_key.empty()) {
|
|
|
+ // Special key with Meta, e.g. M-ArrowUp?
|
|
|
+ // resolve_key returns "ArrowUp", so we get "M-ArrowUp". This is valid.
|
|
|
+ key_name = "M-" + next_key;
|
|
|
+ } else {
|
|
|
+ // Couldn't resolve next key, just treat as Escape then ignore next?
|
|
|
+ // Or treat as Escape sequence.
|
|
|
+ key_name = "Escape";
|
|
|
+ // We effectively consumed next_ch and ignored it.
|
|
|
+ // Better might be to ungetch, but ncurses ungetch is tricky.
|
|
|
+ // Let's assume if resolve_key fails it was garbage.
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ key_name = "Escape";
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ key_name = resolve_key(ch);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (key_name.empty()) {
|
|
|
+ debug_log << "Empty key name, ignoring input" << std::endl;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ debug_log << "Resolved key: " << key_name << std::endl;
|
|
|
+
|
|
|
+ // Handle Minibuffer Input Logic first
|
|
|
+ if (core_->minibuffer_manager().is_active()) {
|
|
|
+ return core_->minibuffer_manager().handle_key_event(key_name);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Normal mode processing (pass to keybinding system)
|
|
|
+ KeyProcessingResult result = core_->keybinding_manager().process_key(Key::parse(key_name));
|
|
|
+
|
|
|
+ if (result.command_result.has_value()) {
|
|
|
+ core_->set_message(result.command_result->message);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result.type == KeyResult::Unbound) {
|
|
|
+ // Fallback: Self-insert for printable characters
|
|
|
+ // Check if key is a single character and not a control sequence
|
|
|
+ // The resolve_key function returns "C-x", "M-x", "Esc", "Return", etc.
|
|
|
+ // Printable characters are returned as single chars "a", "1", etc.
|
|
|
+ bool has_ctrl = key_name.find("C-") != std::string::npos;
|
|
|
+ bool has_meta = key_name.find("M-") != std::string::npos;
|
|
|
+
|
|
|
+ if (!has_ctrl && !has_meta && key_name.length() == 1) {
|
|
|
+ // We can assume it's printable if length is 1 and it's not a special key (which resolve_key handles)
|
|
|
+ core_->command_system().execute("self-insert-command", {key_name});
|
|
|
+
|
|
|
+ // --- Macro Recording Logic for Self-Insert ---
|
|
|
+ if (core_->is_recording_macro()) {
|
|
|
+ core_->record_key_sequence(key_name);
|
|
|
+ }
|
|
|
+ // --------------------------------------------
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result.type != KeyResult::Unbound;
|
|
|
+}
|
|
|
+
|
|
|
+// process_key is removed as keybinding_manager handles it
|
|
|
+
|
|
|
+
|
|
|
|
|
|
void TuiEditor::render() {
|
|
|
// Reset hardware cursor position
|
|
|
@@ -110,7 +432,45 @@ void TuiEditor::render() {
|
|
|
refresh();
|
|
|
}
|
|
|
|
|
|
-// ... (render_layout_node unchanged) ...
|
|
|
+void TuiEditor::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 separator_width = (width > 2) ? 1 : 0;
|
|
|
+ int available_width = width - separator_width;
|
|
|
+
|
|
|
+ int left_width = available_width / 2;
|
|
|
+ int right_width = available_width - left_width;
|
|
|
+
|
|
|
+ render_layout_node(node->child1, x, y, left_width, height);
|
|
|
+
|
|
|
+ // Draw separator if enabled
|
|
|
+ if (separator_width > 0) {
|
|
|
+ int attrs = get_attributes_for_face("window-divider");
|
|
|
+ attron(attrs);
|
|
|
+
|
|
|
+ int sep_x = x + left_width;
|
|
|
+ for (int i = 0; i < height; ++i) {
|
|
|
+ mvaddch(y + i, sep_x, ACS_VLINE);
|
|
|
+ }
|
|
|
+
|
|
|
+ attroff(attrs);
|
|
|
+ }
|
|
|
+
|
|
|
+ render_layout_node(node->child2, x + left_width + separator_width, y, right_width, height);
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
void TuiEditor::render_window(std::shared_ptr<Window> window, int x, int y, int width, int height) {
|
|
|
if (!window) return;
|