|
|
@@ -4,26 +4,86 @@
|
|
|
#include <ftxui/component/screen_interactive.hpp>
|
|
|
#include <ftxui/dom/elements.hpp>
|
|
|
#include <iostream>
|
|
|
+#include <fstream>
|
|
|
#include <memory>
|
|
|
|
|
|
+// Global debug log
|
|
|
+std::ofstream debug_log("lumacs_debug.log");
|
|
|
+
|
|
|
using namespace ftxui;
|
|
|
using namespace lumacs;
|
|
|
|
|
|
+/// Convert TextAttribute::ColorType to FTXUI Color
|
|
|
+Color color_type_to_ftxui(TextAttribute::ColorType color) {
|
|
|
+ switch (color) {
|
|
|
+ case TextAttribute::ColorType::Keyword:
|
|
|
+ return Color::Blue;
|
|
|
+ case TextAttribute::ColorType::String:
|
|
|
+ return Color::Green;
|
|
|
+ case TextAttribute::ColorType::Comment:
|
|
|
+ return Color::GrayDark;
|
|
|
+ case TextAttribute::ColorType::Function:
|
|
|
+ return Color::Cyan;
|
|
|
+ case TextAttribute::ColorType::Type:
|
|
|
+ return Color::Yellow;
|
|
|
+ case TextAttribute::ColorType::Number:
|
|
|
+ return Color::Magenta;
|
|
|
+ case TextAttribute::ColorType::Operator:
|
|
|
+ return Color::White;
|
|
|
+ case TextAttribute::ColorType::Variable:
|
|
|
+ return Color::White;
|
|
|
+ case TextAttribute::ColorType::Constant:
|
|
|
+ return Color::Magenta;
|
|
|
+ case TextAttribute::ColorType::Error:
|
|
|
+ return Color::Red;
|
|
|
+ case TextAttribute::ColorType::Default:
|
|
|
+ default:
|
|
|
+ return Color::Default;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Apply style flags to an FTXUI Element
|
|
|
+Element apply_style_flags(Element elem, int style_flags) {
|
|
|
+ if (style_flags & static_cast<int>(TextAttribute::Style::Bold)) {
|
|
|
+ elem = elem | bold;
|
|
|
+ }
|
|
|
+ if (style_flags & static_cast<int>(TextAttribute::Style::Italic)) {
|
|
|
+ // FTXUI doesn't have italic, use dim as approximation
|
|
|
+ elem = elem | dim;
|
|
|
+ }
|
|
|
+ if (style_flags & static_cast<int>(TextAttribute::Style::Underline)) {
|
|
|
+ elem = elem | underlined;
|
|
|
+ }
|
|
|
+ return elem;
|
|
|
+}
|
|
|
+
|
|
|
/// FTXUI-based TUI frontend for Lumacs
|
|
|
class TuiEditor {
|
|
|
public:
|
|
|
- TuiEditor() : lua_api_(core_) {
|
|
|
+ TuiEditor() {
|
|
|
+ // Initialize Core first
|
|
|
+ core_ = std::make_unique<EditorCore>();
|
|
|
+ // Then LuaApi, which depends on Core
|
|
|
+ lua_api_ = std::make_unique<LuaApi>(*core_);
|
|
|
+
|
|
|
// Listen to editor events
|
|
|
- core_.on_event([this](EditorEvent event) {
|
|
|
+ core_->on_event([this](EditorEvent event) {
|
|
|
handle_editor_event(event);
|
|
|
});
|
|
|
|
|
|
// Load init.lua configuration
|
|
|
- lua_api_.load_init_file();
|
|
|
+ lua_api_->load_init_file();
|
|
|
+ }
|
|
|
+
|
|
|
+ ~TuiEditor() {
|
|
|
+ // Explicitly destroy Core (and its Buffers/Callbacks) BEFORE LuaApi
|
|
|
+ // This prevents callbacks from trying to access a closed Lua state
|
|
|
+ core_.reset();
|
|
|
+ lua_api_.reset();
|
|
|
}
|
|
|
|
|
|
void load_file(const std::filesystem::path& path) {
|
|
|
- if (!core_.load_file(path)) {
|
|
|
+ if (!core_->load_file(path)) {
|
|
|
std::cerr << "Failed to load file: " << path << std::endl;
|
|
|
}
|
|
|
}
|
|
|
@@ -41,11 +101,21 @@ public:
|
|
|
}
|
|
|
|
|
|
private:
|
|
|
- EditorCore core_;
|
|
|
- LuaApi lua_api_;
|
|
|
+ enum class Mode {
|
|
|
+ Normal,
|
|
|
+ Command // Minibuffer entry
|
|
|
+ };
|
|
|
+
|
|
|
+ std::unique_ptr<EditorCore> core_;
|
|
|
+ std::unique_ptr<LuaApi> lua_api_;
|
|
|
ScreenInteractive* screen_ = nullptr;
|
|
|
bool should_quit_ = false;
|
|
|
std::string message_line_; // For displaying messages in the UI
|
|
|
+
|
|
|
+ // Command mode state
|
|
|
+ Mode mode_ = Mode::Normal;
|
|
|
+ std::string command_buffer_;
|
|
|
+ bool meta_pending_ = false; // Emacs-style ESC prefix support
|
|
|
|
|
|
void handle_editor_event(EditorEvent event) {
|
|
|
if (event == EditorEvent::Quit) {
|
|
|
@@ -53,52 +123,194 @@ private:
|
|
|
if (screen_) {
|
|
|
screen_->Exit();
|
|
|
}
|
|
|
+ } else if (event == EditorEvent::Message) {
|
|
|
+ message_line_ = core_->last_message();
|
|
|
+ // Trigger re-render? The main loop usually handles it if it came from input.
|
|
|
+ // But if it came from elsewhere, we might need screen_->Post(Event::Custom)
|
|
|
+ } else if (event == EditorEvent::CommandMode) {
|
|
|
+ mode_ = Mode::Command;
|
|
|
+ command_buffer_.clear();
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /// Render a line with syntax highlighting
|
|
|
+ Element render_styled_line(lumacs::Window& window, const std::string& line_text, size_t line_num,
|
|
|
+ size_t cursor_col, bool has_cursor) {
|
|
|
+ const auto& styles = window.buffer().get_line_styles(line_num);
|
|
|
+
|
|
|
+ // If no styles, render as plain text
|
|
|
+ if (styles.empty()) {
|
|
|
+ if (has_cursor) {
|
|
|
+ return render_line_with_cursor(line_text, cursor_col);
|
|
|
+ }
|
|
|
+ return text(line_text);
|
|
|
+ }
|
|
|
|
|
|
- bool handle_input(Event event) {
|
|
|
- // Convert FTXUI event to string key for Lua bindings
|
|
|
- std::string key_name;
|
|
|
+ // Build segments with styles
|
|
|
+ Elements segments;
|
|
|
+ size_t pos = 0;
|
|
|
+
|
|
|
+ for (const auto& styled : styles) {
|
|
|
+ size_t start = styled.range.start.column;
|
|
|
+ size_t end = styled.range.end.column;
|
|
|
+
|
|
|
+ // Add unstyled text before this styled range
|
|
|
+ if (pos < start) {
|
|
|
+ std::string unstyled = line_text.substr(pos, start - pos);
|
|
|
+ if (has_cursor && cursor_col >= pos && cursor_col < start) {
|
|
|
+ // Cursor is in unstyled part
|
|
|
+ segments.push_back(render_line_with_cursor(unstyled, cursor_col - pos));
|
|
|
+ } else {
|
|
|
+ segments.push_back(text(unstyled));
|
|
|
+ }
|
|
|
+ pos = start;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add styled text
|
|
|
+ if (pos < end && pos < line_text.size()) {
|
|
|
+ size_t segment_end = std::min(end, line_text.size());
|
|
|
+ std::string styled_text = line_text.substr(pos, segment_end - pos);
|
|
|
|
|
|
- // Handle control characters first (they're not is_character())
|
|
|
- if (!event.input().empty()) {
|
|
|
- int code = static_cast<int>(static_cast<unsigned char>(event.input()[0]));
|
|
|
+ Element elem;
|
|
|
+ if (has_cursor && cursor_col >= pos && cursor_col < segment_end) {
|
|
|
+ // Cursor is in styled part
|
|
|
+ elem = render_line_with_cursor(styled_text, cursor_col - pos);
|
|
|
+ } else {
|
|
|
+ elem = text(styled_text);
|
|
|
+ }
|
|
|
|
|
|
- // Control characters (Ctrl+A through Ctrl+Z)
|
|
|
- if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
|
|
|
- // Skip tab(9), newline(10), return(13) - handle them specially
|
|
|
- char letter = 'a' + (code - 1);
|
|
|
- key_name = "C-" + std::string(1, letter);
|
|
|
+ // Apply color and style
|
|
|
+ auto color = color_type_to_ftxui(styled.attr.color);
|
|
|
+ elem = elem | ftxui::color(color);
|
|
|
+ elem = apply_style_flags(elem, styled.attr.style_flags);
|
|
|
+
|
|
|
+ segments.push_back(elem);
|
|
|
+ pos = segment_end;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Detect Meta/Alt modifier by checking raw input first
|
|
|
- // Alt+ArrowUp sends ESC[1;3A (bytes: 27 91 49 59 51 65)
|
|
|
+ // Add remaining unstyled text
|
|
|
+ if (pos < line_text.size()) {
|
|
|
+ std::string remaining = line_text.substr(pos);
|
|
|
+ if (has_cursor && cursor_col >= pos) {
|
|
|
+ segments.push_back(render_line_with_cursor(remaining, cursor_col - pos));
|
|
|
+ } else {
|
|
|
+ segments.push_back(text(remaining));
|
|
|
+ }
|
|
|
+ } else if (has_cursor && cursor_col >= line_text.size()) {
|
|
|
+ // Cursor at end of line
|
|
|
+ segments.push_back(text(" ") | inverted);
|
|
|
+ }
|
|
|
+
|
|
|
+ return hbox(segments);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Render a line segment with cursor highlighting
|
|
|
+ Element render_line_with_cursor(const std::string& text_str, size_t cursor_pos) {
|
|
|
+ if (cursor_pos >= text_str.size()) {
|
|
|
+ return hbox({text(text_str), text(" ") | inverted});
|
|
|
+ }
|
|
|
+
|
|
|
+ std::string before = text_str.substr(0, cursor_pos);
|
|
|
+ std::string at_cursor = text_str.substr(cursor_pos, 1);
|
|
|
+ std::string after = text_str.substr(cursor_pos + 1);
|
|
|
+
|
|
|
+ return hbox({
|
|
|
+ text(before),
|
|
|
+ text(at_cursor) | inverted,
|
|
|
+ text(after)
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ bool handle_input(Event event) {
|
|
|
+ // Debug input in hex
|
|
|
+ std::string input_debug = event.input();
|
|
|
+ debug_log << "Input Event: is_char=" << event.is_character() << " size=" << input_debug.size() << " bytes=[";
|
|
|
+ for(unsigned char c : input_debug) debug_log << std::hex << (int)c << " ";
|
|
|
+ debug_log << std::dec << "]" << std::endl;
|
|
|
+
|
|
|
+ // Handle Command Mode Input
|
|
|
+ if (mode_ == Mode::Command) {
|
|
|
+ if (event == Event::Escape) {
|
|
|
+ mode_ = Mode::Normal;
|
|
|
+ command_buffer_.clear();
|
|
|
+ message_line_ = "Cancelled";
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::Return) {
|
|
|
+ execute_command(command_buffer_);
|
|
|
+ mode_ = Mode::Normal;
|
|
|
+ command_buffer_.clear();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event == Event::Backspace) {
|
|
|
+ if (!command_buffer_.empty()) {
|
|
|
+ command_buffer_.pop_back();
|
|
|
+ } else {
|
|
|
+ // Backspace on empty buffer exits command mode
|
|
|
+ mode_ = Mode::Normal;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (event.is_character()) {
|
|
|
+ command_buffer_ += event.input();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ // Allow arrows to fall through to default navigation or specific handling if we added history?
|
|
|
+ // For now, just consume
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Convert FTXUI event to string key for Lua bindings
|
|
|
+ std::string key_name;
|
|
|
std::string input_str = event.input();
|
|
|
|
|
|
- // Check for Alt+Arrow sequences before FTXUI event processing
|
|
|
- if (input_str.size() >= 6 &&
|
|
|
- input_str[0] == 27 && input_str[1] == 91 &&
|
|
|
- input_str[2] == 49 && input_str[3] == 59 && input_str[4] == 51) {
|
|
|
- // Alt modifier detected (ESC[1;3X pattern)
|
|
|
- char arrow_key = input_str[5];
|
|
|
- switch (arrow_key) {
|
|
|
- case 65: key_name = "M-ArrowUp"; break; // A
|
|
|
- case 66: key_name = "M-ArrowDown"; break; // B
|
|
|
- case 67: key_name = "M-ArrowRight"; break; // C
|
|
|
- case 68: key_name = "M-ArrowLeft"; break; // D
|
|
|
+ // 1. Handle Meta Pending state (previous key was ESC)
|
|
|
+ if (meta_pending_) {
|
|
|
+ meta_pending_ = false; // One-shot prefix
|
|
|
+
|
|
|
+ if (event == Event::Escape) {
|
|
|
+ // ESC ESC -> treat as single Escape (Cancel/Quit)
|
|
|
+ key_name = "Escape";
|
|
|
+ } else if (event.is_character()) {
|
|
|
+ key_name = "M-" + event.input();
|
|
|
+ } else if (event == Event::ArrowUp) key_name = "M-ArrowUp";
|
|
|
+ else if (event == Event::ArrowDown) key_name = "M-ArrowDown";
|
|
|
+ else if (event == Event::ArrowLeft) key_name = "M-ArrowLeft";
|
|
|
+ else if (event == Event::ArrowRight) key_name = "M-ArrowRight";
|
|
|
+ else {
|
|
|
+ // Unknown special key with meta... ignore or try best effort?
|
|
|
+ // Fallback to normal processing effectively ignoring the previous ESC
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Check for Raw Meta sequences (Alt+Key sending \x1b + char in one event)
|
|
|
+ if (key_name.empty()) {
|
|
|
+ if (input_str.size() >= 2 && input_str[0] == 27 && input_str[1] >= 32 && input_str[1] < 127) {
|
|
|
+ // Atomic Alt+Char
|
|
|
+ key_name = "M-" + std::string(1, input_str[1]);
|
|
|
+ } else if (input_str.size() >= 6 && input_str.substr(0,5) == "\x1b[1;3") {
|
|
|
+ // Atomic Alt+Arrow (simplified check)
|
|
|
+ if (event == Event::ArrowUp) key_name = "M-ArrowUp";
|
|
|
+ if (event == Event::ArrowDown) key_name = "M-ArrowDown";
|
|
|
+ if (event == Event::ArrowRight) key_name = "M-ArrowRight";
|
|
|
+ if (event == Event::ArrowLeft) key_name = "M-ArrowLeft";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Handle special keys
|
|
|
+ // 3. Handle standard Control/Special keys if not yet resolved
|
|
|
if (key_name.empty()) {
|
|
|
if (event == Event::Escape) {
|
|
|
- key_name = "Escape";
|
|
|
+ // Don't act immediately, set pending
|
|
|
+ meta_pending_ = true;
|
|
|
+ message_line_ = "M- (Press key)";
|
|
|
+ return true;
|
|
|
} else if (event == Event::ArrowUp) {
|
|
|
key_name = "ArrowUp";
|
|
|
} else if (event == Event::ArrowDown) {
|
|
|
key_name = "ArrowDown";
|
|
|
- } else if (event == Event::ArrowLeft) {
|
|
|
+ }
|
|
|
+ else if (event == Event::ArrowLeft) {
|
|
|
key_name = "ArrowLeft";
|
|
|
} else if (event == Event::ArrowRight) {
|
|
|
key_name = "ArrowRight";
|
|
|
@@ -111,170 +323,318 @@ private:
|
|
|
} else if (event == Event::Tab) {
|
|
|
key_name = "Tab";
|
|
|
} else if (event.is_character()) {
|
|
|
- // Regular printable characters
|
|
|
- key_name = event.input();
|
|
|
+ // Check control chars
|
|
|
+ if (!input_str.empty()) {
|
|
|
+ int code = static_cast<int>(static_cast<unsigned char>(input_str[0]));
|
|
|
+ if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
|
|
|
+ char letter = 'a' + (code - 1);
|
|
|
+ key_name = "C-" + std::string(1, letter);
|
|
|
+ } else {
|
|
|
+ key_name = input_str;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Debug: show what key was pressed
|
|
|
if (!key_name.empty()) {
|
|
|
message_line_ = "Key: " + key_name;
|
|
|
+ debug_log << "Resolved Key: " << key_name << std::endl;
|
|
|
}
|
|
|
|
|
|
// Try Lua key binding first
|
|
|
- if (!key_name.empty() && lua_api_.execute_key_binding(key_name)) {
|
|
|
+ if (!key_name.empty() && lua_api_->execute_key_binding(key_name)) {
|
|
|
message_line_ = "Executed: " + key_name;
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
// Fallback to default bindings
|
|
|
- // Quit commands
|
|
|
- if (event == Event::Escape || event == Event::Character('Q')) {
|
|
|
- core_.request_quit();
|
|
|
- return true;
|
|
|
+
|
|
|
+ // Quit commands (Explicit Q binding if generic quit failed)
|
|
|
+ // Note: Escape is now handled by meta_pending logic. To quit via Escape, press ESC ESC.
|
|
|
+ // Or we can check if generic "Escape" key_name was produced (from double esc).
|
|
|
+ if (key_name == "Escape") {
|
|
|
+ core_->request_quit();
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
// Ctrl+C to quit
|
|
|
if (event.is_character()) {
|
|
|
auto input = event.input();
|
|
|
if (!input.empty() && input[0] == 3) { // Ctrl+C
|
|
|
- core_.request_quit();
|
|
|
+ core_->request_quit();
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Arrow keys for cursor movement
|
|
|
if (event == Event::ArrowUp) {
|
|
|
- core_.move_up();
|
|
|
+ core_->move_up();
|
|
|
return true;
|
|
|
}
|
|
|
if (event == Event::ArrowDown) {
|
|
|
- core_.move_down();
|
|
|
+ core_->move_down();
|
|
|
return true;
|
|
|
}
|
|
|
if (event == Event::ArrowLeft) {
|
|
|
- core_.move_left();
|
|
|
+ core_->move_left();
|
|
|
return true;
|
|
|
}
|
|
|
if (event == Event::ArrowRight) {
|
|
|
- core_.move_right();
|
|
|
+ core_->move_right();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Vim-style movement (h, j, k, l)
|
|
|
- if (event == Event::Character('h')) {
|
|
|
- core_.move_left();
|
|
|
+ // Home/End
|
|
|
+ if (event == Event::Home) {
|
|
|
+ core_->move_to_line_start();
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::Character('j')) {
|
|
|
- core_.move_down();
|
|
|
+ if (event == Event::End) {
|
|
|
+ core_->move_to_line_end();
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::Character('k')) {
|
|
|
- core_.move_up();
|
|
|
+
|
|
|
+ // Backspace
|
|
|
+ if (event == Event::Backspace) {
|
|
|
+ auto cursor = core_->cursor();
|
|
|
+ core_->buffer().erase_char(cursor);
|
|
|
+ // Move cursor back if we deleted a character
|
|
|
+ if (cursor.column > 0) {
|
|
|
+ core_->set_cursor({cursor.line, cursor.column - 1});
|
|
|
+ } else if (cursor.line > 0) {
|
|
|
+ // Moved to end of previous line
|
|
|
+ size_t prev_line_len = core_->buffer().line(cursor.line - 1).size();
|
|
|
+ core_->set_cursor({cursor.line - 1, prev_line_len});
|
|
|
+ }
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::Character('l')) {
|
|
|
- core_.move_right();
|
|
|
+
|
|
|
+ // Delete key (delete character at cursor)
|
|
|
+ if (event == Event::Delete) {
|
|
|
+ auto cursor = core_->cursor();
|
|
|
+ // Delete the character after cursor by erasing at cursor+1
|
|
|
+ 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) {
|
|
|
+ // At end of line, join with next line
|
|
|
+ core_->buffer().erase_char({cursor.line + 1, 0});
|
|
|
+ }
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
- // Home/End
|
|
|
- if (event == Event::Home) {
|
|
|
- core_.move_to_line_start();
|
|
|
+ // Return/Enter - insert newline
|
|
|
+ if (event == Event::Return) {
|
|
|
+ auto cursor = core_->cursor();
|
|
|
+ core_->buffer().insert_newline(cursor);
|
|
|
+ // Move cursor to start of next line
|
|
|
+ core_->set_cursor({cursor.line + 1, 0});
|
|
|
return true;
|
|
|
}
|
|
|
- if (event == Event::End) {
|
|
|
- core_.move_to_line_end();
|
|
|
+
|
|
|
+ // Tab - insert tab character or spaces
|
|
|
+ if (event == Event::Tab) {
|
|
|
+ auto cursor = core_->cursor();
|
|
|
+ // Insert 4 spaces (or you could insert '\t')
|
|
|
+ core_->buffer().insert(cursor, " ");
|
|
|
+ core_->set_cursor({cursor.line, cursor.column + 4});
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ // Regular printable characters
|
|
|
+ if (event.is_character()) {
|
|
|
+ std::string input = event.input();
|
|
|
+ if (!input.empty() && input[0] >= 32 && input[0] < 127) {
|
|
|
+ // Printable ASCII character
|
|
|
+ auto cursor = core_->cursor();
|
|
|
+ core_->buffer().insert_char(cursor, input[0]);
|
|
|
+ // Move cursor forward
|
|
|
+ core_->set_cursor({cursor.line, cursor.column + 1});
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
return false;
|
|
|
}
|
|
|
+
|
|
|
+ void execute_command(const std::string& cmd) {
|
|
|
+ if (cmd.empty()) return;
|
|
|
+
|
|
|
+ if (cmd == "q" || cmd == "quit") {
|
|
|
+ core_->request_quit();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (cmd == "w" || cmd == "write") {
|
|
|
+ core_->buffer().save();
|
|
|
+ message_line_ = "Saved";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (cmd == "wq") {
|
|
|
+ core_->buffer().save();
|
|
|
+ core_->request_quit();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try executing as Lua
|
|
|
+ if (lua_api_->execute(cmd)) {
|
|
|
+ message_line_ = "Lua Executed";
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ message_line_ = "Lua Error (check stderr)";
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- Element render() {
|
|
|
- const auto& buffer = core_.buffer();
|
|
|
- const auto& cursor = core_.cursor();
|
|
|
-
|
|
|
- // Get the current terminal dimensions
|
|
|
- auto dimensions = Terminal::Size();
|
|
|
- // Account for border (2 lines) + status line (2 lines including separator)
|
|
|
- int viewport_height = std::max(1, dimensions.dimy - 4);
|
|
|
- core_.set_viewport_size(dimensions.dimx, viewport_height);
|
|
|
-
|
|
|
+ // Helper to render a single window
|
|
|
+ Element render_window(lumacs::Window& window, int width, int height) {
|
|
|
+ debug_log << "render_window: " << width << "x" << height << std::endl;
|
|
|
+
|
|
|
+ const auto& buffer = window.buffer();
|
|
|
+ const auto& cursor = window.cursor();
|
|
|
+ bool is_active = (core_->active_window().get() == &window);
|
|
|
+
|
|
|
+ // Calculate available height for text lines
|
|
|
+ // Total height - 2 (border) - 1 (status)
|
|
|
+ int lines_height = std::max(0, height - 3);
|
|
|
+
|
|
|
+ // Get visible range for this window
|
|
|
+ auto [start_line, end_line] = window.visible_line_range();
|
|
|
const auto& buffer_lines = buffer.lines();
|
|
|
- auto [start_line, end_line] = core_.visible_line_range();
|
|
|
-
|
|
|
+
|
|
|
Elements lines;
|
|
|
-
|
|
|
+
|
|
|
// Render visible lines
|
|
|
for (size_t i = start_line; i < end_line; ++i) {
|
|
|
const auto& line_text = buffer_lines[i];
|
|
|
|
|
|
- // Build line with cursor highlighting
|
|
|
+ // Build line
|
|
|
Elements line_elements;
|
|
|
-
|
|
|
- // Add line number
|
|
|
+
|
|
|
+ // Line number
|
|
|
line_elements.push_back(text(std::to_string(i + 1) + " │ "));
|
|
|
|
|
|
- // Render line with cursor on the current line
|
|
|
- if (i == cursor.line) {
|
|
|
- // Split line into: before cursor | at cursor | after cursor
|
|
|
- std::string before = line_text.substr(0, cursor.column);
|
|
|
- std::string at_cursor;
|
|
|
- std::string after;
|
|
|
-
|
|
|
- if (cursor.column < line_text.size()) {
|
|
|
- // Cursor is on a character
|
|
|
- at_cursor = line_text.substr(cursor.column, 1);
|
|
|
- after = line_text.substr(cursor.column + 1);
|
|
|
- } else {
|
|
|
- // Cursor is at end of line (show as space)
|
|
|
- at_cursor = " ";
|
|
|
- }
|
|
|
-
|
|
|
- line_elements.push_back(text(before));
|
|
|
- line_elements.push_back(text(at_cursor) | inverted); // Highlight cursor
|
|
|
- line_elements.push_back(text(after));
|
|
|
- } else {
|
|
|
- // Regular line without cursor
|
|
|
- line_elements.push_back(text(line_text));
|
|
|
- }
|
|
|
+ // Styled line
|
|
|
+ bool has_cursor = (is_active && i == cursor.line && mode_ == Mode::Normal);
|
|
|
+ line_elements.push_back(render_styled_line(window, line_text, i, cursor.column, has_cursor));
|
|
|
|
|
|
lines.push_back(hbox(line_elements));
|
|
|
}
|
|
|
-
|
|
|
- // Fill remaining space if viewport is larger than content
|
|
|
- while (lines.size() < static_cast<size_t>(viewport_height)) {
|
|
|
+
|
|
|
+ // Fill remaining space
|
|
|
+ while (lines.size() < static_cast<size_t>(lines_height)) {
|
|
|
lines.push_back(text("~"));
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// Status line
|
|
|
std::string status = buffer.name() +
|
|
|
(buffer.is_modified() ? " [+]" : "") +
|
|
|
- " | Line " + std::to_string(cursor.line + 1) +
|
|
|
- "/" + std::to_string(buffer.line_count()) +
|
|
|
- " Col " + std::to_string(cursor.column + 1);
|
|
|
+ " | " + std::to_string(cursor.line + 1) +
|
|
|
+ ":" + std::to_string(cursor.column + 1);
|
|
|
+
|
|
|
+ if (is_active && mode_ == Mode::Command) status += " [CMD]";
|
|
|
+
|
|
|
+ auto status_elem = text(status) | inverted;
|
|
|
+ if (!is_active) status_elem = status_elem | dim;
|
|
|
+
|
|
|
+ return vbox({
|
|
|
+ vbox(lines) | flex,
|
|
|
+ status_elem
|
|
|
+ }) | border | size(WIDTH, EQUAL, width) | size(HEIGHT, EQUAL, height);
|
|
|
+ }
|
|
|
|
|
|
- // Show scroll indicator if there's more content
|
|
|
- if (buffer_lines.size() > static_cast<size_t>(viewport_height)) {
|
|
|
- int percent = (cursor.line * 100) / (buffer_lines.size() - 1);
|
|
|
- status += " " + std::to_string(percent) + "%";
|
|
|
+ void update_layout_sizes(LayoutNode* node, int w, int h) {
|
|
|
+ debug_log << "update_layout_sizes: node=" << (int)node->type << " w=" << w << " h=" << h << std::endl;
|
|
|
+
|
|
|
+ if (node->type == LayoutNode::Type::Leaf) {
|
|
|
+ // Account for border (2 lines) and status (1 line)
|
|
|
+ // render_window produces an element of height: viewport.height + 1 (status) + 2 (border)
|
|
|
+ // We want this to be <= h.
|
|
|
+ // viewport.height + 3 <= h => viewport.height <= h - 3
|
|
|
+ int vp_h = std::max(1, h - 3);
|
|
|
+ int vp_w = std::max(1, w - 2);
|
|
|
+ debug_log << " Leaf: setting viewport to " << vp_w << "x" << vp_h << std::endl;
|
|
|
+ node->window->set_viewport_size(vp_w, vp_h);
|
|
|
+ } else if (node->type == LayoutNode::Type::HorizontalSplit) {
|
|
|
+ // Account for separator (1 line)
|
|
|
+ int available_h = std::max(0, h - 1);
|
|
|
+ int h1 = available_h / 2;
|
|
|
+ int h2 = available_h - h1;
|
|
|
+ debug_log << " HSplit: h1=" << h1 << " h2=" << h2 << std::endl;
|
|
|
+ update_layout_sizes(node->child1.get(), w, h1);
|
|
|
+ update_layout_sizes(node->child2.get(), w, h2);
|
|
|
+ } else { // Vertical
|
|
|
+ // Account for separator (1 column)
|
|
|
+ int available_w = std::max(0, w - 1);
|
|
|
+ int w1 = available_w / 2;
|
|
|
+ int w2 = available_w - w1;
|
|
|
+ debug_log << " VSplit: w1=" << w1 << " w2=" << w2 << std::endl;
|
|
|
+ update_layout_sizes(node->child1.get(), w1, h);
|
|
|
+ update_layout_sizes(node->child2.get(), w2, h);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // Message line (minibuffer)
|
|
|
- Elements ui_elements = {
|
|
|
- vbox(lines) | flex,
|
|
|
- separator(),
|
|
|
- text(status) | inverted
|
|
|
- };
|
|
|
-
|
|
|
- // Show message line if there's a message
|
|
|
- if (!message_line_.empty()) {
|
|
|
- ui_elements.push_back(text(message_line_) | dim);
|
|
|
+ Element render_node(LayoutNode* node) {
|
|
|
+ if (node->type == LayoutNode::Type::Leaf) {
|
|
|
+ // dimensions are updated in update_layout_sizes
|
|
|
+ const auto& vp = node->window->viewport();
|
|
|
+ // Reconstruct approximated outer size for render_window helper
|
|
|
+ // content height = vp.height. outer = height + 3
|
|
|
+ return render_window(*node->window, vp.width + 2, vp.height + 3);
|
|
|
+ } else if (node->type == LayoutNode::Type::HorizontalSplit) {
|
|
|
+ return vbox({
|
|
|
+ render_node(node->child1.get()) | flex,
|
|
|
+ separator(),
|
|
|
+ render_node(node->child2.get()) | flex
|
|
|
+ });
|
|
|
+ } else { // Vertical
|
|
|
+ return hbox({
|
|
|
+ render_node(node->child1.get()) | flex,
|
|
|
+ separator(),
|
|
|
+ render_node(node->child2.get()) | flex
|
|
|
+ });
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- return vbox(ui_elements) | border;
|
|
|
+ Element render() {
|
|
|
+ // Dimensions
|
|
|
+ auto term_size = Terminal::Size();
|
|
|
+ debug_log << "Render Frame. Term Size: " << term_size.dimx << "x" << term_size.dimy << std::endl;
|
|
|
+
|
|
|
+ int height = std::max(1, term_size.dimy - 1); // -1 for minibuffer/message line
|
|
|
+
|
|
|
+ // Calculate layout sizes
|
|
|
+ update_layout_sizes(core_->root_layout().get(), term_size.dimx, height);
|
|
|
+
|
|
|
+ // Render Tree
|
|
|
+ Element editor_area = render_node(core_->root_layout().get());
|
|
|
+
|
|
|
+ // Minibuffer / Message Line
|
|
|
+ Element bottom_bar;
|
|
|
+
|
|
|
+ if (mode_ == Mode::Command) {
|
|
|
+ debug_log << " Mode: Command" << std::endl;
|
|
|
+ bottom_bar = hbox({
|
|
|
+ text(":") | bold | color(Color::Yellow),
|
|
|
+ text(command_buffer_),
|
|
|
+ text(" ") | inverted // Fake cursor
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ debug_log << " Mode: Normal" << std::endl;
|
|
|
+ if (!message_line_.empty()) {
|
|
|
+ bottom_bar = text(message_line_) | dim;
|
|
|
+ } else {
|
|
|
+ bottom_bar = text("");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Force bottom bar to take exactly 1 line
|
|
|
+ bottom_bar = bottom_bar | size(HEIGHT, EQUAL, 1);
|
|
|
+
|
|
|
+ return vbox({
|
|
|
+ editor_area | flex,
|
|
|
+ bottom_bar
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
};
|
|
|
@@ -290,8 +650,9 @@ int main(int argc, char* argv[]) {
|
|
|
|
|
|
auto ui = editor.create_ui(&screen);
|
|
|
|
|
|
- std::cout << "Lumacs - A modern text editor" << std::endl;
|
|
|
- std::cout << "Controls: Arrow keys or hjkl to move, Q or Esc to quit" << std::endl;
|
|
|
+ std::cout << "Lumacs - A modern text editor with Lua scripting" << std::endl;
|
|
|
+ std::cout << "Navigation: Arrow keys | Editing: Type, Backspace, Enter, Tab" << std::endl;
|
|
|
+ std::cout << "Commands: Ctrl+S=save, Ctrl+L=highlight, Esc/Q=quit" << std::endl;
|
|
|
std::cout << std::endl;
|
|
|
|
|
|
screen.Loop(ui);
|