|
|
@@ -1,801 +1,88 @@
|
|
|
#include "lumacs/editor_core.hpp"
|
|
|
-#include "lumacs/lua_api.hpp"
|
|
|
-#include <ftxui/component/component.hpp>
|
|
|
-#include <ftxui/component/screen_interactive.hpp>
|
|
|
-#include <ftxui/dom/elements.hpp>
|
|
|
+#include "lumacs/tui_editor.hpp"
|
|
|
+#include "lumacs/gtk_editor.hpp"
|
|
|
#include <iostream>
|
|
|
-#include <fstream>
|
|
|
-#include <memory>
|
|
|
-#include <chrono>
|
|
|
-#include <termios.h>
|
|
|
-#include <unistd.h>
|
|
|
+#include <string>
|
|
|
+#include <vector>
|
|
|
+#include <filesystem>
|
|
|
|
|
|
-// 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;
|
|
|
+void print_help() {
|
|
|
+ std::cout << "Usage: lumacs [options] [file]\n"
|
|
|
+ << "Options:\n"
|
|
|
+ << " -nw, --no-window Run in terminal mode (TUI)\n"
|
|
|
+ << " --help Show this help message\n";
|
|
|
}
|
|
|
|
|
|
-/// FTXUI-based TUI frontend for Lumacs
|
|
|
-class TuiEditor {
|
|
|
-public:
|
|
|
- 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) {
|
|
|
- handle_editor_event(event);
|
|
|
- });
|
|
|
-
|
|
|
- // Load init.lua configuration
|
|
|
- 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)) {
|
|
|
- std::cerr << "Failed to load file: " << path << std::endl;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- Component create_ui(ScreenInteractive* screen) {
|
|
|
- screen_ = screen;
|
|
|
- auto renderer = Renderer([this] { return render(); });
|
|
|
-
|
|
|
- // Add keyboard event handling
|
|
|
- auto component = CatchEvent(renderer, [this](Event event) {
|
|
|
- return handle_input(event);
|
|
|
- });
|
|
|
-
|
|
|
- return component;
|
|
|
- }
|
|
|
-
|
|
|
-private:
|
|
|
- enum class Mode {
|
|
|
- Normal,
|
|
|
- Command, // Minibuffer entry (:)
|
|
|
- FindFile // Find file prompt (C-x C-f)
|
|
|
- };
|
|
|
-
|
|
|
- enum class Prefix {
|
|
|
- None,
|
|
|
- Meta, // Alt/Meta pressed (ESC+key sequence)
|
|
|
- CtrlX // C-x pressed
|
|
|
- };
|
|
|
-
|
|
|
- struct PendingPrefix {
|
|
|
- Prefix type = Prefix::None;
|
|
|
- std::chrono::steady_clock::time_point timestamp;
|
|
|
- static constexpr auto TIMEOUT = std::chrono::milliseconds(500);
|
|
|
- };
|
|
|
-
|
|
|
- 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/Input state
|
|
|
- Mode mode_ = Mode::Normal;
|
|
|
- std::string command_buffer_;
|
|
|
- PendingPrefix pending_prefix_;
|
|
|
-
|
|
|
- bool is_prefix_expired() const {
|
|
|
- if (pending_prefix_.type == Prefix::None) return false;
|
|
|
- auto now = std::chrono::steady_clock::now();
|
|
|
- return (now - pending_prefix_.timestamp) > PendingPrefix::TIMEOUT;
|
|
|
- }
|
|
|
-
|
|
|
- void handle_editor_event(EditorEvent event) {
|
|
|
- if (event == EditorEvent::Quit) {
|
|
|
- should_quit_ = true;
|
|
|
- if (screen_) {
|
|
|
- screen_->Exit();
|
|
|
- }
|
|
|
- } else if (event == EditorEvent::Message) {
|
|
|
- message_line_ = core_->last_message();
|
|
|
- } else if (event == EditorEvent::CommandMode) {
|
|
|
- mode_ = Mode::Command;
|
|
|
- command_buffer_.clear();
|
|
|
- } else if (event == EditorEvent::WindowFocused) {
|
|
|
- // No specific action needed here, just re-render handled by main loop.
|
|
|
- // But it's good to have a dedicated event for this.
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /// 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);
|
|
|
- }
|
|
|
-
|
|
|
- // 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);
|
|
|
-
|
|
|
- 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);
|
|
|
- }
|
|
|
-
|
|
|
- // 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;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 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));
|
|
|
+int main(int argc, char* argv[]) {
|
|
|
+ bool force_tui = false;
|
|
|
+ std::string filename;
|
|
|
+
|
|
|
+ // Parse arguments
|
|
|
+ for (int i = 1; i < argc; ++i) {
|
|
|
+ std::string arg = argv[i];
|
|
|
+ if (arg == "-nw" || arg == "--no-window") {
|
|
|
+ force_tui = true;
|
|
|
+ } else if (arg == "--help") {
|
|
|
+ print_help();
|
|
|
+ return 0;
|
|
|
+ } else {
|
|
|
+ // Assume it's a filename
|
|
|
+ if (filename.empty()) {
|
|
|
+ filename = arg;
|
|
|
}
|
|
|
- } 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 with more details
|
|
|
- std::string input_debug = event.input();
|
|
|
- debug_log << "=== INPUT DEBUG ===" << std::endl;
|
|
|
- debug_log << "Event type: ";
|
|
|
- if (event == Event::Escape) debug_log << "Escape";
|
|
|
- else if (event == Event::Return) debug_log << "Return";
|
|
|
- else if (event == Event::Tab) debug_log << "Tab";
|
|
|
- else if (event == Event::Backspace) debug_log << "Backspace";
|
|
|
- else if (event == Event::Delete) debug_log << "Delete";
|
|
|
- else if (event == Event::ArrowUp) debug_log << "ArrowUp";
|
|
|
- else if (event == Event::ArrowDown) debug_log << "ArrowDown";
|
|
|
- else if (event == Event::ArrowLeft) debug_log << "ArrowLeft";
|
|
|
- else if (event == Event::ArrowRight) debug_log << "ArrowRight";
|
|
|
- else debug_log << "Other";
|
|
|
- debug_log << std::endl;
|
|
|
- debug_log << "is_character: " << event.is_character() << std::endl;
|
|
|
- debug_log << "input_size: " << input_debug.size() << std::endl;
|
|
|
- debug_log << "bytes: [";
|
|
|
- for(unsigned char c : input_debug) debug_log << std::hex << (int)c << " ";
|
|
|
- debug_log << std::dec << "]" << std::endl;
|
|
|
- debug_log << "chars: \"" << input_debug << "\"" << std::endl;
|
|
|
- debug_log << "===================" << std::endl;
|
|
|
-
|
|
|
- // Handle Minibuffer Inputs (Command / FindFile)
|
|
|
- if (mode_ == Mode::Command || mode_ == Mode::FindFile) {
|
|
|
- if (event == Event::Escape) {
|
|
|
- mode_ = Mode::Normal;
|
|
|
- command_buffer_.clear();
|
|
|
- message_line_ = "Cancelled";
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (event == Event::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 (event == Event::Backspace) {
|
|
|
- if (!command_buffer_.empty()) {
|
|
|
- command_buffer_.pop_back();
|
|
|
- } else {
|
|
|
- mode_ = Mode::Normal;
|
|
|
- }
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (event.is_character()) {
|
|
|
- command_buffer_ += event.input();
|
|
|
- return true;
|
|
|
- }
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // Check for expired prefix first
|
|
|
- if (is_prefix_expired()) {
|
|
|
- pending_prefix_.type = Prefix::None;
|
|
|
- }
|
|
|
-
|
|
|
- // === Key Resolution ===
|
|
|
- std::string key_name;
|
|
|
- std::string input_str = event.input();
|
|
|
- bool is_meta_sequence = false;
|
|
|
-
|
|
|
- // 1. Detect Meta sequences (Alt/ESC + key)
|
|
|
- if (input_str.size() >= 2 && input_str[0] == 27) {
|
|
|
- // Debug the meta sequence we got
|
|
|
- debug_log << "Meta sequence detected: size=" << input_str.size() << " bytes=";
|
|
|
- for (size_t i = 0; i < input_str.size(); ++i) {
|
|
|
- debug_log << std::hex << (int)(unsigned char)input_str[i] << " ";
|
|
|
- }
|
|
|
- debug_log << std::dec << std::endl;
|
|
|
-
|
|
|
- // ESC followed by another key - this is a Meta sequence
|
|
|
- if (input_str.size() == 3 && input_str[1] == '[') {
|
|
|
- // Meta+Arrow keys: ESC[A, ESC[B, ESC[C, ESC[D
|
|
|
- switch (input_str[2]) {
|
|
|
- case 'A': key_name = "M-ArrowUp"; break;
|
|
|
- case 'B': key_name = "M-ArrowDown"; break;
|
|
|
- case 'C': key_name = "M-ArrowRight"; break;
|
|
|
- case 'D': key_name = "M-ArrowLeft"; break;
|
|
|
- default:
|
|
|
- debug_log << "Unknown ESC[ sequence: " << input_str[2] << std::endl;
|
|
|
- break;
|
|
|
- }
|
|
|
- is_meta_sequence = true;
|
|
|
- } else if (input_str.size() >= 6 && input_str.substr(1, 5) == "[1;3") {
|
|
|
- // Alternative Meta+Arrow format: ESC[1;3A, ESC[1;3B, ESC[1;3C, ESC[1;3D
|
|
|
- if (input_str.size() == 6) {
|
|
|
- switch (input_str[5]) {
|
|
|
- case 'A': key_name = "M-ArrowUp"; break;
|
|
|
- case 'B': key_name = "M-ArrowDown"; break;
|
|
|
- case 'C': key_name = "M-ArrowRight"; break;
|
|
|
- case 'D': key_name = "M-ArrowLeft"; break;
|
|
|
- default:
|
|
|
- debug_log << "Unknown ESC[1;3 sequence: " << input_str[5] << std::endl;
|
|
|
- break;
|
|
|
- }
|
|
|
- is_meta_sequence = true;
|
|
|
- }
|
|
|
- } else if (input_str[1] != '[') {
|
|
|
- // Regular Meta+char (not arrow keys)
|
|
|
- key_name = "M-" + std::string(1, input_str[1]);
|
|
|
- is_meta_sequence = true;
|
|
|
- }
|
|
|
- // If it's ESC[ but not a recognized pattern, let it fall through to normal arrow key handling
|
|
|
- }
|
|
|
-
|
|
|
- // 2. Basic Key Mapping (if not Meta sequence)
|
|
|
- if (!is_meta_sequence) {
|
|
|
- if (event == Event::Escape) key_name = "Escape";
|
|
|
- else if (event == Event::ArrowUp) key_name = "ArrowUp";
|
|
|
- else if (event == Event::ArrowDown) key_name = "ArrowDown";
|
|
|
- else if (event == Event::ArrowLeft) key_name = "ArrowLeft";
|
|
|
- else if (event == Event::ArrowRight) key_name = "ArrowRight";
|
|
|
- else if (event == Event::Home) key_name = "Home";
|
|
|
- else if (event == Event::End) key_name = "End";
|
|
|
- else if (event == Event::Return) key_name = "Return";
|
|
|
- else if (event == Event::Tab) key_name = "Tab";
|
|
|
- else if (event == Event::Backspace) key_name = "Backspace";
|
|
|
- else if (event == Event::Delete) key_name = "Delete";
|
|
|
- else {
|
|
|
- // Check based on input content (handles Chars, Control keys)
|
|
|
- if (!input_str.empty()) {
|
|
|
- int code = static_cast<int>(static_cast<unsigned char>(input_str[0]));
|
|
|
- debug_log << "Character code analysis: " << code << std::endl;
|
|
|
- if (code > 0 && code < 27 && code != 9 && code != 10 && code != 13) {
|
|
|
- char letter = 'a' + (code - 1);
|
|
|
- key_name = "C-" + std::string(1, letter);
|
|
|
- debug_log << "Detected control key: " << key_name << std::endl;
|
|
|
- } else if (code >= 32 || code < 0) { // Printable (including extended ASCII/UTF-8 starts)
|
|
|
- key_name = input_str;
|
|
|
- debug_log << "Detected printable char: " << key_name << std::endl;
|
|
|
- } else {
|
|
|
- debug_log << "Unhandled character code: " << code << std::endl;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 3. Apply Pending Prefixes
|
|
|
- if (pending_prefix_.type == Prefix::Meta && !is_meta_sequence) {
|
|
|
- pending_prefix_.type = Prefix::None;
|
|
|
- if (key_name.size() == 1) { // Single char
|
|
|
- key_name = "M-" + key_name;
|
|
|
- } else if (key_name == "ArrowUp") key_name = "M-ArrowUp";
|
|
|
- else if (key_name == "ArrowDown") key_name = "M-ArrowDown";
|
|
|
- else if (key_name == "ArrowLeft") key_name = "M-ArrowLeft";
|
|
|
- else if (key_name == "ArrowRight") key_name = "M-ArrowRight";
|
|
|
- // For other special keys, just prefix with M-
|
|
|
- else if (!key_name.empty()) key_name = "M-" + key_name;
|
|
|
- }
|
|
|
- else if (pending_prefix_.type == Prefix::CtrlX) {
|
|
|
- pending_prefix_.type = Prefix::None;
|
|
|
- key_name = "C-x " + key_name;
|
|
|
- }
|
|
|
+ try {
|
|
|
+ // Initialize Core
|
|
|
+ EditorCore core;
|
|
|
|
|
|
- // Debug: show what key was resolved
|
|
|
- if (!key_name.empty()) {
|
|
|
- debug_log << "Resolved Key: " << key_name << std::endl;
|
|
|
- debug_log << "Pending prefix: " << static_cast<int>(pending_prefix_.type) << std::endl;
|
|
|
- } else {
|
|
|
- debug_log << "No key resolved for input: size=" << input_str.size() << std::endl;
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- // === Prefix Trigger Handling ===
|
|
|
- // Handle C-x prefix (but not Escape - let it be a normal key)
|
|
|
- if (key_name == "C-x") {
|
|
|
- pending_prefix_.type = Prefix::CtrlX;
|
|
|
- pending_prefix_.timestamp = std::chrono::steady_clock::now();
|
|
|
- message_line_ = "C-x-";
|
|
|
- debug_log << "Set C-x prefix" << std::endl;
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // Handle single Escape as Meta prefix only if no other Meta sequence detected
|
|
|
- if (key_name == "Escape" && !is_meta_sequence && pending_prefix_.type == Prefix::None) {
|
|
|
- pending_prefix_.type = Prefix::Meta;
|
|
|
- pending_prefix_.timestamp = std::chrono::steady_clock::now();
|
|
|
- message_line_ = "M-";
|
|
|
- debug_log << "Set Meta prefix" << std::endl;
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // === Execution ===
|
|
|
+ // Select Frontend
|
|
|
+ std::unique_ptr<IEditorView> view;
|
|
|
|
|
|
- // 1. Try Lua key binding
|
|
|
- debug_log << "Trying Lua binding for: " << key_name << std::endl;
|
|
|
- if (lua_api_->execute_key_binding(key_name)) {
|
|
|
- debug_log << "Lua binding executed successfully" << std::endl;
|
|
|
- return true;
|
|
|
+#ifdef LUMACS_WITH_GTK
|
|
|
+ if (!force_tui) {
|
|
|
+ view = create_gtk_editor();
|
|
|
}
|
|
|
- debug_log << "No Lua binding found, trying C++ fallbacks" << std::endl;
|
|
|
+#endif
|
|
|
|
|
|
- // 2. C++ Fallback Bindings
|
|
|
-
|
|
|
- // Global
|
|
|
- if (key_name == "C-q") {
|
|
|
- core_->request_quit();
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (key_name == "C-s") {
|
|
|
- core_->buffer().save();
|
|
|
- message_line_ = "Saved";
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (key_name == "M-x") {
|
|
|
- mode_ = Mode::Command;
|
|
|
- message_line_ = ":";
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (key_name == ":") { // Vim style
|
|
|
- mode_ = Mode::Command;
|
|
|
- message_line_ = ":";
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // C-x Prefix Commands (commented out - using Lua bindings instead)
|
|
|
- // if (key_name == "C-x o") {
|
|
|
- // core_->next_window();
|
|
|
- // return true;
|
|
|
- // }
|
|
|
- // if (key_name == "C-x 2") {
|
|
|
- // core_->split_horizontally();
|
|
|
- // return true;
|
|
|
- // }
|
|
|
- // if (key_name == "C-x 3") {
|
|
|
- // core_->split_vertically();
|
|
|
- // return true;
|
|
|
- // }
|
|
|
- // if (key_name == "C-x 0") {
|
|
|
- // core_->close_active_window();
|
|
|
- // return true;
|
|
|
- // }
|
|
|
- if (key_name == "C-x C-f") {
|
|
|
- mode_ = Mode::FindFile;
|
|
|
- command_buffer_.clear();
|
|
|
- message_line_ = "Find file: ";
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- // Editing / Navigation
|
|
|
- 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; }
|
|
|
-
|
|
|
- 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});
|
|
|
+ // Fallback to TUI if GTK not available or TUI forced
|
|
|
+ if (!view) {
|
|
|
+ if (!force_tui && !view) {
|
|
|
+ // If we tried to load GTK and failed (e.g. create_gtk_editor returned null), warn user?
|
|
|
+ // For now, create_gtk_editor prints to stderr if built without GTK.
|
|
|
}
|
|
|
- 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
|
|
|
- if (key_name.size() == 1) {
|
|
|
- auto cursor = core_->cursor();
|
|
|
- core_->buffer().insert_char(cursor, key_name[0]);
|
|
|
- core_->set_cursor({cursor.line, cursor.column + 1});
|
|
|
- return true;
|
|
|
+ view = create_tui_editor();
|
|
|
}
|
|
|
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- void execute_command(const std::string& cmd) {
|
|
|
- if (cmd.empty()) return;
|
|
|
-
|
|
|
- // Simple command parsing (first word is command, rest is args)
|
|
|
- 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); // Read rest of line
|
|
|
- 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";
|
|
|
+ if (!view) {
|
|
|
+ std::cerr << "Failed to create editor interface." << std::endl;
|
|
|
+ return 1;
|
|
|
}
|
|
|
- else {
|
|
|
- message_line_ = "Lua Error (check stderr)";
|
|
|
- }
|
|
|
- }
|
|
|
|
|
|
- // 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();
|
|
|
-
|
|
|
- Elements lines;
|
|
|
-
|
|
|
- // Render visible lines
|
|
|
- for (size_t i = start_line; i < end_line; ++i) {
|
|
|
- const auto& line_text = buffer_lines[i];
|
|
|
-
|
|
|
- // Build line
|
|
|
- Elements line_elements;
|
|
|
-
|
|
|
- // Line number
|
|
|
- line_elements.push_back(text(std::to_string(i + 1) + " │ "));
|
|
|
-
|
|
|
- // 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
|
|
|
- while (lines.size() < static_cast<size_t>(lines_height)) {
|
|
|
- lines.push_back(text("~"));
|
|
|
- }
|
|
|
-
|
|
|
- // Status line
|
|
|
- std::string status = buffer.name() +
|
|
|
- (buffer.is_modified() ? " [+]" : "") +
|
|
|
- " | " + 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;
|
|
|
-
|
|
|
- auto window_content = vbox({
|
|
|
- vbox(lines) | flex,
|
|
|
- status_elem
|
|
|
+ // Link Core and View
|
|
|
+ view->set_core(&core);
|
|
|
+ core.on_event([&view](EditorEvent event) {
|
|
|
+ view->handle_editor_event(event);
|
|
|
});
|
|
|
|
|
|
- if (is_active) {
|
|
|
- return window_content | border | color(Color::Cyan) | bold | size(WIDTH, EQUAL, width) | size(HEIGHT, EQUAL, height);
|
|
|
- } else {
|
|
|
- return window_content | border | size(WIDTH, EQUAL, width) | size(HEIGHT, EQUAL, height);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- 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);
|
|
|
- }
|
|
|
- }
|
|
|
+ // Initialize View
|
|
|
+ view->init();
|
|
|
|
|
|
- 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
|
|
|
- });
|
|
|
+ // Load initial file if provided
|
|
|
+ if (!filename.empty()) {
|
|
|
+ core.load_file(filename);
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- Element render() {
|
|
|
- // Dimensions
|
|
|
- auto term_size = Terminal::Size();
|
|
|
- debug_log << "Render Frame. Term Size: " << term_size.dimx << "x" << term_size.dimy << std::endl;
|
|
|
+ // Run main loop
|
|
|
+ view->run();
|
|
|
|
|
|
- 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 if (mode_ == Mode::FindFile) {
|
|
|
- debug_log << " Mode: FindFile" << std::endl;
|
|
|
- bottom_bar = hbox({
|
|
|
- text("Find file: ") | bold | color(Color::Cyan),
|
|
|
- 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
|
|
|
- });
|
|
|
+ } catch (const std::exception& e) {
|
|
|
+ std::cerr << "Fatal Error: " << e.what() << std::endl;
|
|
|
+ return 1;
|
|
|
}
|
|
|
|
|
|
-};
|
|
|
-
|
|
|
-// Function to configure terminal for raw input
|
|
|
-void configure_terminal_for_control_chars() {
|
|
|
- struct termios tty;
|
|
|
- if (tcgetattr(STDIN_FILENO, &tty) == 0) {
|
|
|
- // Disable flow control (Ctrl+S, Ctrl+Q)
|
|
|
- tty.c_iflag &= ~(IXON | IXOFF | IXANY);
|
|
|
-
|
|
|
- // Apply settings
|
|
|
- tcsetattr(STDIN_FILENO, TCSANOW, &tty);
|
|
|
-
|
|
|
- debug_log << "Terminal configured for control char capture" << std::endl;
|
|
|
- } else {
|
|
|
- debug_log << "Failed to configure terminal" << std::endl;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-int main(int argc, char* argv[]) {
|
|
|
- // Configure terminal to allow more control characters
|
|
|
- configure_terminal_for_control_chars();
|
|
|
-
|
|
|
- auto screen = ScreenInteractive::Fullscreen();
|
|
|
- TuiEditor editor;
|
|
|
-
|
|
|
- // Load file if provided
|
|
|
- if (argc > 1) {
|
|
|
- editor.load_file(argv[1]);
|
|
|
- }
|
|
|
-
|
|
|
- auto ui = editor.create_ui(&screen);
|
|
|
-
|
|
|
- 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);
|
|
|
return 0;
|
|
|
-}
|
|
|
+}
|