| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182 |
- #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:
- 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<Range> isearch_match_;
- bool isearch_failed_ = false;
- // Completion state
- std::vector<std::string> completion_candidates_;
- size_t completion_index_ = 0;
- std::string completion_prefix_;
- // Minibuffer history
- std::vector<std::string> command_history_;
- std::vector<std::string> buffer_switch_history_;
- std::vector<std::string> kill_buffer_history_;
- std::vector<std::string> 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<std::string>& 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<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_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<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 content_height = height_ - 2;
- 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) { // 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<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;
- }
- }
- }
-
- debug_log << "Resolved key: '" << key_name << "'" << std::endl;
- debug_log << "============================" << std::endl;
-
- return key_name;
- }
- // History management
- std::vector<std::string>& 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<std::string> 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<std::vector<std::string>>();
- }
- } 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<Range> 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<char>(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<char>(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 <filename>";
- }
- 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<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
- 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> 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<IEditorView> create_tui_editor() {
- return std::make_unique<TuiEditor>();
- }
- }
|