| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562 |
- #include "lumacs/tui_editor.hpp"
- #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>
- #include <algorithm>
- // 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:
- EditorCore* core_ = nullptr; // Raw pointer to EditorCore, not owned
- bool should_quit_ = false;
- std::string message_line_;
- int height_ = 0, width_ = 0;
-
- // Private helper method declarations
- std::string resolve_key(int ch);
- bool handle_input(int ch);
- void render();
- void render_layout_node(std::shared_ptr<LayoutNode> node, int x, int y, int width, int height);
- void render_window(std::shared_ptr<Window> window, int x, int y, int width, int height);
- void render_window_modeline(std::shared_ptr<Window> window, int x, int y, int width, bool is_active);
- 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 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();
-
- 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;
- 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) {
- 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::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_->command_system().execute(input);
- core_->set_message(result.message);
- }
- },
- [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_->set_theme(input);
- core_->set_message("Switched to theme: " + input);
- } else {
- core_->set_message("Theme not found: " + input);
- }
- },
- [this]() { core_->set_message("Cancelled"); }
- );
- } else if (event == EditorEvent::ISearchMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::ISearch, "I-search: ",
- [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search query: " + input); },
- [this]() { core_->set_message("Cancelled I-search"); }
- );
- } else if (event == EditorEvent::ISearchBackwardMode) {
- core_->minibuffer_manager().activate_minibuffer(
- MinibufferMode::ISearch, "I-search backward: ",
- [this](const std::string& input) { /* TODO: Implement actual isearch logic */ core_->set_message("I-search backward query: " + input); },
- [this]() { core_->set_message("Cancelled I-search backward"); }
- );
- } 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 ---
- /// 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;
-
- bool TuiEditor::handle_input(int ch) {
- // Resolve key name
- std::string key_name = resolve_key(ch);
-
- if (key_name.empty()) {
- debug_log << "Empty key name, ignoring input" << std::endl;
- return false;
- }
- // 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)
- return core_->keybinding_manager().process_key(Key::parse(key_name)) != KeyResult::Unbound;
- }
- // process_key is removed as keybinding_manager handles it
- 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 and potentially a completion line)
- int minibuffer_lines = 1; // Always reserve 1 line for minibuffer/message
- // if (core_->minibuffer_manager().is_active() && !core_->minibuffer_manager().get_completion_candidates().empty()) {
- // minibuffer_lines++; // Reserve an extra line for completions
- // }
- int content_height = height_ - minibuffer_lines;
- 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<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;
-
- // Check configuration for line numbers and modeline
- bool show_line_numbers = core_->config().get<bool>("show_line_numbers", true);
- bool show_modeline = core_->config().get<bool>("show_modeline", true);
- int line_number_width = show_line_numbers ? core_->config().get<int>("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 - now handled by MinibufferManager
- // 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) {
- 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> 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_message_line() {
- int msg_y = height_ - 1;
- int attrs = get_attributes_for_face("minibuffer-prompt");
-
- attron(attrs);
- move(msg_y, 0);
- clrtoeol();
- if (core_->minibuffer_manager().is_active()) {
- std::string prompt_part = core_->minibuffer_manager().get_prompt();
- std::string input_part = core_->minibuffer_manager().get_input_buffer();
- std::string display_text = prompt_part + input_part;
-
- mvprintw(msg_y, 0, "%s", display_text.c_str());
- // Display completion candidates below the input line
- auto candidates = core_->minibuffer_manager().get_completion_candidates();
- if (!candidates.empty()) {
- std::string completion_display;
- for (size_t i = 0; i < candidates.size() && completion_display.length() < width_ - 5; ++i) {
- if (!completion_display.empty()) completion_display += " ";
- completion_display += candidates[i].display_text; // Use display_text
- }
- if (completion_display.length() >= width_ - 5) {
- completion_display = completion_display.substr(0, width_ - 8) + "...";
- }
- // Move up one line to display completions above the current minibuffer line
- mvprintw(msg_y - 1, 0, "%s", completion_display.c_str());
- }
- } else if (!message_line_.empty()) {
- // Display transient message
- mvprintw(msg_y, 0, "%s", message_line_.c_str());
- }
-
- attroff(attrs);
- // After rendering, check if it's time to clear the message.
- core_->check_and_clear_message();
- }
- namespace lumacs {
- std::unique_ptr<IEditorView> create_tui_editor() {
- return std::make_unique<TuiEditor>();
- }
- }
|