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