#include "lumacs/tui_editor.hpp" #include "lumacs/editor_core.hpp" #include "lumacs/lua_api.hpp" #include #include #include #include #include #include #include #include // Global debug log extern std::ofstream debug_log; std::ofstream debug_log("lumacs_debug.log"); using namespace lumacs; /// ncurses-based TUI frontend for Lumacs class TuiEditor : public IEditorView { public: TuiEditor() : core_(nullptr) {} ~TuiEditor() override { endwin(); // Cleanup ncurses } // IEditorView methods void init() override; void run() override; void handle_editor_event(EditorEvent event) override; void set_core(EditorCore* core) override; private: enum class Mode { Normal, Command, // Minibuffer entry FindFile, // Find file prompt BufferSwitch, // Buffer switching with completion KillBuffer, // Kill buffer with completion ConfirmKill, // Confirm killing modified buffer ISearch // Incremental search }; EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned bool should_quit_ = false; std::string message_line_; int height_ = 0, width_ = 0; // Input state Mode mode_ = Mode::Normal; std::string command_buffer_; // ISearch state std::string isearch_query_; bool isearch_forward_ = true; Position isearch_start_pos_; std::optional isearch_match_; bool isearch_failed_ = false; // Completion state std::vector completion_candidates_; size_t completion_index_ = 0; std::string completion_prefix_; // Minibuffer history std::vector command_history_; std::vector buffer_switch_history_; std::vector kill_buffer_history_; std::vector isearch_history_; size_t history_index_ = 0; // Meta key handling bool waiting_for_meta_ = false; std::chrono::steady_clock::time_point meta_time_; static constexpr auto META_TIMEOUT = std::chrono::milliseconds(100); // Private helper method declarations std::string resolve_key(int ch); std::vector& get_current_history(); void add_to_history(const std::string& entry); void previous_history(); void next_history(); void reset_history_navigation(); void update_completion_candidates(const std::string& prefix); void reset_completion(); void perform_search(bool find_next); bool handle_input(int ch); bool process_key(const std::string& key_name); void execute_command(const std::string& cmd); int get_attributes_for_face(const std::string& face_name); void render(); void render_layout_node(std::shared_ptr node, int x, int y, int width, int height); void render_window(std::shared_ptr window, int x, int y, int width, int height); void render_window_modeline(std::shared_ptr window, int x, int y, int width, bool is_active); void render_status_line(); void render_message_line(); }; // --- TuiEditor Public Method Definitions --- 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_->active_theme()) { core_->active_theme()->initialize_ncurses_colors(); } // Set initial viewport size (leave room for status and message lines) int content_height = height_ - 2; // -1 for status, -1 for message bool show_line_numbers = core_->config().get("show_line_numbers", true); int line_number_width = show_line_numbers ? core_->config().get("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(); 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; bool show_line_numbers = core_->config().get("show_line_numbers", true); int line_number_width = show_line_numbers ? core_->config().get("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; 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(); } } } void TuiEditor::handle_editor_event(EditorEvent event) { // existing method 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(); reset_completion(); reset_history_navigation(); } else if (event == EditorEvent::BufferSwitchMode) { mode_ = Mode::BufferSwitch; command_buffer_.clear(); reset_completion(); reset_history_navigation(); } else if (event == EditorEvent::KillBufferMode) { mode_ = Mode::KillBuffer; command_buffer_.clear(); reset_completion(); reset_history_navigation(); } else if (event == EditorEvent::FindFileMode) { mode_ = Mode::FindFile; command_buffer_.clear(); reset_completion(); reset_history_navigation(); } else if (event == EditorEvent::ISearchMode) { mode_ = Mode::ISearch; isearch_query_.clear(); isearch_forward_ = true; isearch_start_pos_ = core_->cursor(); isearch_match_ = std::nullopt; isearch_failed_ = false; } else if (event == EditorEvent::ISearchBackwardMode) { mode_ = Mode::ISearch; isearch_query_.clear(); isearch_forward_ = false; isearch_start_pos_ = core_->cursor(); isearch_match_ = std::nullopt; isearch_failed_ = false; } } void TuiEditor::set_core(EditorCore* core) { core_ = core; } // --- TuiEditor Private Helper Method Definitions --- /// 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; 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; case KEY_F(3): key_name = "F3"; break; case KEY_F(4): key_name = "F4"; break; default: debug_log << "Unknown special key: " << ch << " (ignoring)" << std::endl; // Return empty string to ignore unknown special keys return ""; } } 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 >= 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(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; } } } debug_log << "Resolved key: '" << key_name << "'" << std::endl; debug_log << "============================" << std::endl; return key_name; } // History management std::vector& TuiEditor::get_current_history() { switch (mode_) { case Mode::Command: return command_history_; case Mode::BufferSwitch: return buffer_switch_history_; case Mode::KillBuffer: return kill_buffer_history_; case Mode::ISearch: return isearch_history_; default: return command_history_; } } void TuiEditor::add_to_history(const std::string& entry) { if (entry.empty()) return; auto& history = get_current_history(); // Remove if already exists (move to front) auto it = std::find(history.begin(), history.end(), entry); if (it != history.end()) { history.erase(it); } // Add to front history.insert(history.begin(), entry); // Limit history size const size_t MAX_HISTORY = 100; if (history.size() > MAX_HISTORY) { history.resize(MAX_HISTORY); } } void TuiEditor::previous_history() { auto& history = get_current_history(); if (history.empty()) return; if (history_index_ < history.size()) { command_buffer_ = history[history_index_]; history_index_++; } } void TuiEditor::next_history() { auto& history = get_current_history(); if (history.empty() || history_index_ == 0) return; history_index_--; if (history_index_ == 0) { command_buffer_ = ""; } else { command_buffer_ = history[history_index_ - 1]; } } void TuiEditor::reset_history_navigation() { history_index_ = 0; } void TuiEditor::update_completion_candidates(const std::string& prefix) { std::vector candidates; if (mode_ == Mode::Command) { // Get command names from Lua auto& lua = core_->lua_api()->state(); sol::function get_names = lua["get_command_names"]; if (get_names.valid()) { candidates = get_names.call>(); } } else { // Default to buffer names for BufferSwitch/KillBuffer candidates = core_->get_buffer_names(); } completion_candidates_.clear(); if (prefix.empty()) { completion_candidates_ = candidates; } else { for (const auto& name : candidates) { if (name.size() >= prefix.size() && name.substr(0, prefix.size()) == prefix) { completion_candidates_.push_back(name); } } } completion_index_ = 0; } // Helper to reset completion state void TuiEditor::reset_completion() { completion_candidates_.clear(); completion_index_ = 0; completion_prefix_.clear(); } void TuiEditor::perform_search(bool find_next) { if (isearch_query_.empty()) { isearch_match_ = std::nullopt; isearch_failed_ = false; return; } Position start_search = core_->cursor(); if (find_next) { if (isearch_forward_) { // Forward: move cursor forward 1 char to find next if (start_search.column < core_->buffer().line(start_search.line).size()) { start_search.column++; } else if (start_search.line < core_->buffer().line_count() - 1) { start_search.line++; start_search.column = 0; } } else { // Backward: move cursor backward 1 char if (start_search.column > 0) { start_search.column--; } else if (start_search.line > 0) { start_search.line--; start_search.column = core_->buffer().line(start_search.line).size(); } } } std::optional result; if (isearch_forward_) { result = core_->buffer().find(isearch_query_, start_search); } else { result = core_->buffer().find_backward(isearch_query_, start_search); } if (result) { isearch_match_ = result; isearch_failed_ = false; core_->set_cursor(result->start); core_->adjust_scroll(); } else { isearch_failed_ = true; } } bool TuiEditor::handle_input(int ch) { // Handle confirmation mode if (mode_ == Mode::ConfirmKill) { if (ch == 'y' || ch == 'Y') { if (core_->close_buffer(command_buffer_)) { message_line_ = "Closed modified buffer: " + command_buffer_; } else { message_line_ = "Failed to close buffer"; } mode_ = Mode::Normal; command_buffer_.clear(); reset_completion(); } else if (ch == 'n' || ch == 'N' || ch == 27) { // n or ESC mode_ = Mode::Normal; message_line_ = "Cancelled kill buffer"; command_buffer_.clear(); reset_completion(); } return true; } // Handle ISearch if (mode_ == Mode::ISearch) { // C-g (7) or ESC (27) -> Cancel if (ch == 27 || ch == 7) { core_->set_cursor(isearch_start_pos_); mode_ = Mode::Normal; message_line_ = "Quit"; return true; } // RET -> Accept if (ch == '\n' || ch == '\r') { mode_ = Mode::Normal; message_line_ = "Mark saved"; core_->buffer().set_mark(isearch_start_pos_); return true; } // C-s (19) -> Next if (ch == 19) { isearch_forward_ = true; perform_search(true); return true; } // C-r (18) -> Prev if (ch == 18) { isearch_forward_ = false; perform_search(true); return true; } // Backspace if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { if (!isearch_query_.empty()) { isearch_query_.pop_back(); perform_search(false); } return true; } // Printable if (ch >= 32 && ch <= 126) { isearch_query_ += static_cast(ch); perform_search(false); return true; } // Other keys -> Exit and process mode_ = Mode::Normal; return handle_input(ch); } // Handle minibuffer/command mode if (mode_ == Mode::Command || mode_ == Mode::FindFile || mode_ == Mode::BufferSwitch || mode_ == Mode::KillBuffer) { // ESC - cancel if (ch == 27) { mode_ = Mode::Normal; command_buffer_.clear(); reset_completion(); message_line_ = "Cancelled"; return true; } // TAB - completion if (ch == '\t' && (mode_ == Mode::BufferSwitch || mode_ == Mode::KillBuffer || mode_ == Mode::Command)) { if (completion_candidates_.empty()) { // First TAB: save prefix and get candidates completion_prefix_ = command_buffer_; update_completion_candidates(completion_prefix_); if (!completion_candidates_.empty()) { command_buffer_ = completion_candidates_[0]; completion_index_ = 0; } else { message_line_ = "No matches"; } } else { // Cycle through candidates completion_index_ = (completion_index_ + 1) % completion_candidates_.size(); command_buffer_ = completion_candidates_[completion_index_]; } return true; } // Return - execute if (ch == '\n' || ch == '\r') { // Add to history before execution add_to_history(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_; core_->lua_api()->execute("auto_activate_major_mode()") ; } else { message_line_ = "Failed to load: " + command_buffer_; } } else if (mode_ == Mode::BufferSwitch) { if (core_->switch_buffer_in_window(command_buffer_)) { message_line_ = "Switched to: " + command_buffer_; core_->lua_api()->execute("auto_activate_major_mode()") ; } else { message_line_ = "Buffer not found: " + command_buffer_; } } else if (mode_ == Mode::KillBuffer) { // Check for modification auto buf = core_->get_buffer_by_name(command_buffer_); if (buf && buf->is_modified()) { mode_ = Mode::ConfirmKill; message_line_ = "Buffer modified! Kill anyway? (y/n)"; return true; } if (core_->close_buffer(command_buffer_)) { message_line_ = "Closed buffer: " + command_buffer_; } else { message_line_ = "Failed to close buffer: " + command_buffer_; } } mode_ = Mode::Normal; command_buffer_.clear(); reset_completion(); reset_history_navigation(); return true; } // M-p (Alt+p) - Previous history if (ch == '\x90') { // Meta+p previous_history(); reset_completion(); return true; } // M-n (Alt+n) - Next history if (ch == '\x8E') { // Meta+n next_history(); reset_completion(); return true; } // Backspace if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { if (!command_buffer_.empty()) { command_buffer_.pop_back(); reset_completion(); // Reset completion on edit reset_history_navigation(); // Reset history on edit } else { mode_ = Mode::Normal; } return true; } // Printable characters if (ch >= 32 && ch <= 126) { command_buffer_ += static_cast(ch); reset_completion(); // Reset completion on new input return true; } return true; } // Check for expired meta key if (waiting_for_meta_) { auto now = std::chrono::steady_clock::now(); if (now - meta_time_ > META_TIMEOUT) { debug_log << "Meta timeout, treating ESC as Escape key" << std::endl; waiting_for_meta_ = false; // Process the ESC as a normal Escape key std::string final_key = "Escape"; if (core_->lua_api()->execute_key_binding(final_key)) { core_->record_key_sequence(final_key); return true; } message_line_ = "Key: " + final_key; return false; } } // If we're waiting for a meta key and got one, combine them if (waiting_for_meta_) { waiting_for_meta_ = false; std::string base_key = resolve_key(ch); if (base_key.empty()) { debug_log << "Empty base key after ESC, ignoring" << std::endl; return false; } std::string key_name = "M-" + base_key; debug_log << "Meta sequence complete: " << key_name << std::endl; // Continue processing this meta key below return process_key(key_name); } // Check if this is the start of a meta sequence if (ch == 27) { // ESC waiting_for_meta_ = true; meta_time_ = std::chrono::steady_clock::now(); debug_log << "ESC received, waiting for meta key..." << std::endl; 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; } return process_key(key_name); } bool TuiEditor::process_key(const std::string& key_name) { debug_log << "Processing key: " << key_name << std::endl; // Use the new keybinding system KeyResult result = core_->lua_api()->process_key(key_name); switch (result) { case KeyResult::Executed: debug_log << "Key binding executed successfully" << std::endl; message_line_.clear(); // Clear any partial sequence display // Record the key if we're recording a macro (but not if it's F3/F4 themselves) if (key_name != "F3" && key_name != "F4") { core_->record_key_sequence(key_name); } return true; case KeyResult::Failed: debug_log << "Key binding execution failed" << std::endl; message_line_ = "Command failed"; return true; // Key was handled, even though it failed case KeyResult::Partial: // Building a multi-key sequence message_line_ = core_->keybinding_manager().current_sequence_display(); debug_log << "Partial sequence: " << message_line_ << std::endl; return true; case KeyResult::Timeout: debug_log << "Key sequence timed out" << std::endl; message_line_ = "Sequence timeout"; // Fall through to try fallback bindings break; case KeyResult::Unbound: debug_log << "No key binding found, trying C++ fallbacks" << std::endl; // Fall through to C++ fallback bindings break; } // Clear any sequence display since we're not in a partial state message_line_.clear(); // C++ fallback bindings - these should eventually be moved to Lua // Quit if (key_name == "C-q") { core_->request_quit(); return true; } // Command mode if (key_name == "M-x") { mode_ = Mode::Command; command_buffer_.clear(); return true; } // Navigation fallbacks (these should be in Lua) 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; } // Editing fallbacks (these should also be in Lua) if (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 (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 (key_name == "Return") { auto cursor = core_->cursor(); core_->buffer().insert_newline(cursor); core_->set_cursor({cursor.line + 1, 0}); return true; } if (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 TuiEditor::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; core_->lua_api()->execute("auto_activate_major_mode()") ; } else { message_line_ = "Failed to load: " + path; } } else { message_line_ = "Usage: :e "; } return; } // Try executing via command registry first auto& lua = core_->lua_api()->state(); sol::function exec_cmd = lua["execute_extended_command"]; if (exec_cmd.valid()) { bool result = exec_cmd(cmd); if (result) { return; } } // Fallback: Try executing as Lua code if (core_->lua_api()->execute(cmd)) { message_line_ = "Lua executed"; } else { message_line_ = "Unknown command: " + cmd; } } int TuiEditor::get_attributes_for_face(const std::string& face_name) { auto theme = core_->active_theme(); if (!theme) return 0; return theme->get_face_attributes_ncurses(face_name); } void TuiEditor::render() { // Clear and update screen info getmaxyx(stdscr, height_, width_); // Set background color from theme auto theme = core_->active_theme(); if (theme) { int bg_color_pair = theme->get_color_pair(ThemeElement::Background); bkgd(bg_color_pair); } clear(); // Calculate content area (leave room for message line only) int content_height = height_ - 1; int content_width = width_; // Render the layout tree recursively (now includes per-window modelines) render_layout_node(core_->root_layout(), 0, 0, content_width, content_height); // Global message/command line (last line) render_message_line(); // Refresh screen refresh(); } void TuiEditor::render_layout_node(std::shared_ptr 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, int x, int y, int width, int height) { if (!window) return; // Check configuration for line numbers and modeline bool show_line_numbers = core_->config().get("show_line_numbers", true); bool show_modeline = core_->config().get("show_modeline", true); int line_number_width = show_line_numbers ? core_->config().get("line_number_width", 6) : 0; int modeline_height = show_modeline ? 1 : 0; // Update window viewport size (reserve space for modeline) int content_width = width - line_number_width; int content_height = height - modeline_height; window->set_viewport_size(content_width, content_height); // Get window data const auto& buffer = window->buffer(); const auto cursor = window->cursor(); auto [start_line, end_line] = window->visible_line_range(); 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 < content_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 (if enabled) if (show_line_numbers) { mvprintw(y + screen_y, x, "%3zu ", buffer_line_idx + 1); } // Line content with syntax highlighting if (!line_text.empty()) { int max_content_width = content_width - 1; const auto& styles = buffer.get_line_styles(buffer_line_idx); if (styles.empty()) { // No styles, render as plain text std::string display_text = line_text; 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()); } else { // Render with syntax highlighting size_t pos = 0; int screen_x = x + line_number_width; for (const auto& styled : styles) { size_t start = styled.range.start.column; size_t end = std::min(styled.range.end.column, line_text.size()); // Render unstyled text before this styled range if (pos < start && pos < line_text.size()) { size_t len = std::min(start - pos, line_text.size() - pos); std::string unstyled = line_text.substr(pos, len); mvprintw(y + screen_y, screen_x, "%s", unstyled.c_str()); screen_x += unstyled.length(); pos = start; } // Render styled text if (pos < end && pos < line_text.size()) { size_t len = std::min(end - pos, line_text.size() - pos); std::string styled_text = line_text.substr(pos, len); // Apply color int attrs = get_attributes_for_face(styled.attr.face_name); attron(attrs); mvprintw(y + screen_y, screen_x, "%s", styled_text.c_str()); attroff(attrs); screen_x += styled_text.length(); pos = end; } } // Render remaining unstyled text if (pos < line_text.size()) { std::string remaining = line_text.substr(pos); if ((int)(screen_x - x - line_number_width + remaining.length()) > max_content_width) { remaining = remaining.substr(0, max_content_width - (screen_x - x - line_number_width) - 3) + "..."; } mvprintw(y + screen_y, screen_x, "%s", remaining.c_str()); } } } // Highlight ISearch match if (mode_ == Mode::ISearch && isearch_match_ && isearch_match_->start.line == buffer_line_idx) { size_t match_start = isearch_match_->start.column; size_t match_len = isearch_match_->end.column - match_start; if (match_start < line_text.size()) { size_t display_len = std::min(match_len, line_text.size() - match_start); std::string matched_text = line_text.substr(match_start, display_len); int match_x = x + line_number_width + match_start; // Simple clipping check if (match_x < x + width) { int attrs = get_attributes_for_face(isearch_failed_ ? "isearch-fail" : "isearch"); if (attrs == 0) attrs = A_REVERSE; // Fallback attron(attrs); mvprintw(y + screen_y, match_x, "%s", matched_text.c_str()); attroff(attrs); } } } // 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]; } int attrs = get_attributes_for_face("cursor"); if (attrs == 0) attrs = A_REVERSE; attron(attrs); mvaddch(y + screen_y, cursor_screen_x, cursor_char); attroff(attrs); } } } // Fill remaining lines (for empty lines below buffer) - no tildes size_t displayed_lines = std::min((size_t)content_height, end_line - start_line); for (int screen_y = displayed_lines; screen_y < content_height; ++screen_y) { move(y + screen_y, x); for (int i = 0; i < width; ++i) addch(' '); } // Render modeline for this window if (show_modeline) { render_window_modeline(window, x, y + content_height, width, is_active); } } void TuiEditor::render_window_modeline(std::shared_ptr window, int x, int y, int width, bool is_active) { const auto& buffer = window->buffer(); const auto cursor = window->cursor(); // Choose modeline colors std::string face_name = is_active ? "mode-line" : "mode-line-inactive"; int attrs = get_attributes_for_face(face_name); if (attrs == 0) attrs = is_active ? A_REVERSE : A_DIM; attron(attrs); // Clear the modeline move(y, x); for (int i = 0; i < width; ++i) addch(' '); // Create modeline content std::string modeline; // Buffer name and modification status modeline += buffer.name(); if (buffer.is_modified()) modeline += " [+] "; // Cursor position modeline += " | " + std::to_string(cursor.line + 1) + ":" + std::to_string(cursor.column + 1); // Major mode (if available) // TODO: Add major mode support when available // Right-aligned content (percentage through file) std::string right_side; if (buffer.line_count() > 0) { int percentage = (cursor.line * 100) / (buffer.line_count() - 1); right_side = " " + std::to_string(percentage) + "%"; } // Truncate modeline if too long int available_width = width - right_side.length(); if ((int)modeline.length() > available_width) { modeline = modeline.substr(0, available_width - 3) + "..."; } // Render left side mvprintw(y, x, "%s", modeline.c_str()); // Render right side if (!right_side.empty()) { mvprintw(y, x + width - right_side.length(), "%s", right_side.c_str()); } // Turn off modeline attributes attroff(attrs); } void TuiEditor::render_status_line() { const auto cursor = core_->cursor(); const auto& buffer = core_->buffer(); int status_y = height_ - 2; int attrs = get_attributes_for_face("mode-line"); if (attrs == 0) attrs = A_REVERSE; attron(attrs); 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]"; else if (mode_ == Mode::BufferSwitch) status += " [BUFFER]"; else if (mode_ == Mode::KillBuffer) status += " [KILL]"; else if (mode_ == Mode::ConfirmKill) status += " [CONFIRM]"; else if (mode_ == Mode::ISearch) status += " [I-SEARCH]"; mvprintw(status_y, 0, "%s", status.c_str()); attroff(attrs); } void TuiEditor::render_message_line() { int msg_y = height_ - 1; int attrs = get_attributes_for_face("minibuffer-prompt"); attron(attrs); 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 (mode_ == Mode::BufferSwitch) { std::string prompt = "Switch to buffer: " + command_buffer_; if (!completion_candidates_.empty()) { prompt += " [" + std::to_string(completion_index_ + 1) + "/" + std::to_string(completion_candidates_.size()) + "]"; } mvprintw(msg_y, 0, "%s", prompt.c_str()); } else if (mode_ == Mode::KillBuffer) { std::string prompt = "Kill buffer: " + command_buffer_; if (!completion_candidates_.empty()) { prompt += " [" + std::to_string(completion_index_ + 1) + "/" + std::to_string(completion_candidates_.size()) + "]"; } mvprintw(msg_y, 0, "%s", prompt.c_str()); } else if (mode_ == Mode::ISearch) { std::string prompt = (isearch_failed_ ? "Failing " : "") + std::string("I-search: ") + isearch_query_; mvprintw(msg_y, 0, "%s", prompt.c_str()); } else if (!message_line_.empty()) { mvprintw(msg_y, 0, "%s", message_line_.c_str()); } attroff(attrs); } namespace lumacs { std::unique_ptr create_tui_editor() { return std::make_unique(); } }