Bernardo Magri пре 1 месец
родитељ
комит
013c94a94a

+ 13 - 1
include/lumacs/editor_core.hpp

@@ -142,7 +142,19 @@ public:
     void enter_isearch_backward_mode();
 
     [[nodiscard]] std::vector<BufferManager::BufferInfo> get_all_buffer_info() const { return buffer_manager_->get_all_buffer_info(); }
-    
+
+    /// @brief Get current font size from config
+    [[nodiscard]] int font_size() const { return config_.get<int>("font_size", 12); }
+
+    /// @brief Set font size (triggers FontChanged event)
+    void set_font_size(int size);
+
+    /// @brief Get visual line mode setting
+    [[nodiscard]] bool visual_line_mode() const { return config_.get<bool>("visual_line_mode", false); }
+
+    /// @brief Set visual line mode (soft word wrap)
+    void set_visual_line_mode(bool enabled);
+
     const Viewport& viewport() const noexcept;
     void set_viewport_size(int width, int height);
     void adjust_scroll();

+ 34 - 14
include/lumacs/gtk_renderer.hpp

@@ -53,26 +53,46 @@ public:
     /// @brief Clears the internal render cache, forcing a full redraw.
     /// Useful when the theme changes.
     void invalidate_cache();
-        
-        private:
-            struct LineCacheEntry {
-                std::string text_content;
-                std::vector<StyledRange> styles; // Check if styles changed
-                Cairo::RefPtr<Cairo::ImageSurface> surface;
-                int width = 0;
-                int height = 0;
-            };    
-        // Map Window ID (or ptr) -> (Line Index -> Cache Entry)
-        std::unordered_map<Window*, std::unordered_map<size_t, LineCacheEntry>> render_cache_;
-    
-        EditorCore& core_;
-        Gtk::Widget& context_widget_;
+
+    /// @brief Set the font size in points.
+    void set_font_size(int size);
+
+    /// @brief Get the current font size in points.
+    [[nodiscard]] int font_size() const { return font_size_; }
+
+    /// @brief Get the current line height in pixels.
+    [[nodiscard]] double line_height() const { return line_height_; }
+
+    /// @brief Get the current scale factor (for HiDPI).
+    [[nodiscard]] int scale_factor() const { return scale_factor_; }
+
+private:
+    /// @brief Convert character position to byte index for UTF-8 string.
+    static size_t char_to_byte_index(const std::string& str, size_t char_pos);
+
+    struct LineCacheEntry {
+        std::string text_content;
+        std::vector<StyledRange> styles; // Check if styles changed
+        Cairo::RefPtr<Cairo::ImageSurface> surface;
+        int width = 0;
+        int height = 0;
+        int scale = 1; // Scale factor when surface was created
+    };
+
+    // Map Window ID (or ptr) -> (Line Index -> Cache Entry)
+    std::unordered_map<Window*, std::unordered_map<size_t, LineCacheEntry>> render_cache_;
+
+    EditorCore& core_;
+    Gtk::Widget& context_widget_;
 
     Pango::FontDescription font_desc_;
     bool font_initialized_ = false;
+    int font_size_ = 12;
+    int scale_factor_ = 1; // HiDPI scale factor
     double char_width_ = 0;
     double line_height_ = 0;
     double ascent_ = 0;
+    double descent_ = 0;
 };
 
 } // namespace lumacs

+ 4 - 1
include/lumacs/keybinding.hpp

@@ -176,7 +176,10 @@ public:
     
     /// @brief Set timeout for multi-key sequences.
     void set_sequence_timeout(std::chrono::milliseconds timeout);
-    
+
+    /// @brief Get timeout for multi-key sequences.
+    [[nodiscard]] std::chrono::milliseconds get_sequence_timeout() const { return sequence_timeout_; }
+
     /// @brief Check if current partial sequence has timed out.
     bool has_sequence_timed_out() const;
 

+ 2 - 1
include/lumacs/ui_interface.hpp

@@ -28,7 +28,8 @@ enum class EditorEvent {
     
     TransientMessageCleared, ///< A transient message has been cleared.
     Quit,                 ///< The application should exit.
-    ThemeChanged          ///< The active theme has changed.
+    ThemeChanged,         ///< The active theme has changed.
+    FontChanged           ///< The font size has changed.
 };
 
 /// @brief Abstract interface for a Lumacs editor UI frontend.

+ 69 - 0
init.lua

@@ -10,6 +10,29 @@
 --   5. Override default settings
 -- ============================================================================
 
+-- ============================================================================
+-- PACKAGE SYSTEM INITIALIZATION
+-- ============================================================================
+
+-- Add lua/ to package path for require() to work
+package.path = package.path .. ";lua/?.lua;lua/?/init.lua"
+
+-- Load the package manager
+local pkg = dofile("lua/core/package.lua")
+pkg.setup()
+
+-- Make it globally accessible
+lumacs.package = pkg
+
+-- Load package commands (M-x package-install, etc.)
+dofile("lua/core/package-commands.lua")
+
+-- Load user packages from ~/.lumacs/packages.lua
+local loaded, errors = pkg.load_all()
+if loaded > 0 or errors > 0 then
+    print(string.format("[package] Loaded %d user packages (%d errors)", loaded, errors))
+end
+
 -- ============================================================================
 -- LOAD MAJOR MODES
 -- ============================================================================
@@ -61,6 +84,18 @@ dofile("lua/packages/projectile.lua")
 -- company: Text completion framework
 dofile("lua/packages/company.lua")
 
+-- goto-line: Go to a specific line number (M-g g)
+dofile("lua/packages/goto-line.lua")
+
+-- recentf: Track and access recently opened files
+dofile("lua/packages/recentf.lua")
+
+-- bookmarks: Save and jump between named positions
+dofile("lua/packages/bookmarks.lua")
+
+-- visual-line: Soft word wrap mode
+dofile("lua/packages/visual-line.lua")
+
 -- ============================================================================
 -- MINOR MODES (User-defined extensions)
 -- ============================================================================
@@ -155,6 +190,40 @@ end
 editor:bind_key("M-ArrowUp", swap_line_up)
 editor:bind_key("M-ArrowDown", swap_line_down)
 
+-- ============================================================================
+-- FONT SIZE COMMANDS
+-- ============================================================================
+
+editor:register_command("text-scale-increase", "Increase font size", function(args)
+    local current = editor:font_size()
+    editor:set_font_size(current + 1)
+    return {success = true, message = "Font size: " .. (current + 1)}
+end)
+
+editor:register_command("text-scale-decrease", "Decrease font size", function(args)
+    local current = editor:font_size()
+    editor:set_font_size(current - 1)
+    return {success = true, message = "Font size: " .. (current - 1)}
+end)
+
+editor:register_command("text-scale-set", "Set font size", function(args)
+    if #args == 0 then
+        return {success = true, message = "Current font size: " .. editor:font_size()}
+    end
+    local size = tonumber(args[1])
+    if size then
+        editor:set_font_size(size)
+        return {success = true, message = "Font size set to: " .. size}
+    else
+        return {success = false, message = "Invalid size: " .. args[1]}
+    end
+end, {}, true, "s")
+
+-- Emacs-style font size keybindings (C-x C-+ and C-x C-- in Emacs)
+editor:bind_key("C-x +", "text-scale-increase", "Increase font size")
+editor:bind_key("C-x -", "text-scale-decrease", "Decrease font size")
+editor:bind_key("C-x =", "text-scale-set", "Show/set font size")
+
 -- ============================================================================
 -- CUSTOM COMMANDS
 -- ============================================================================

+ 1 - 1
lua/packages/which-key.lua

@@ -96,7 +96,7 @@ function which_key.get_bindings_for_prefix(prefix)
 
                     table.insert(matching, {
                         key = next_key,
-                        command = binding.command_name,
+                        command = binding.command,
                         description = binding.description,
                         full_sequence = binding.sequence,
                         is_prefix = is_prefix,

+ 5 - 2
src/config.cpp

@@ -31,12 +31,15 @@ void Config::set_defaults() {
     // Display settings
     set("show_line_numbers", true);
     set("line_number_width", 6);
-    
+    set("font_size", 12);  // Default font size in points
+    set("visual_line_mode", false);  // Soft word wrap (like Emacs visual-line-mode)
+    set("word_wrap", true);  // Use word boundaries for wrapping (vs character)
+
     // Editor settings
     set("tab_width", 4);
     set("auto_save", false);
     set("auto_save_interval", 30);  // seconds
-    
+
     // Search settings
     set("case_sensitive_search", false);
     set("incremental_search", true);

+ 12 - 0
src/editor_core.cpp

@@ -909,6 +909,18 @@ void EditorCore::set_theme(const std::string& theme_name) {
     emit_event(EditorEvent::ThemeChanged);
 }
 
+void EditorCore::set_font_size(int size) {
+    if (size < 6) size = 6;
+    if (size > 72) size = 72;
+    config_.set("font_size", size);
+    emit_event(EditorEvent::FontChanged);
+}
+
+void EditorCore::set_visual_line_mode(bool enabled) {
+    config_.set("visual_line_mode", enabled);
+    emit_event(EditorEvent::ViewportChanged);  // Trigger redraw
+}
+
 // === Private ===
 
 void EditorCore::emit_event(EditorEvent event) {

+ 24 - 4
src/gtk_editor.cpp

@@ -19,6 +19,7 @@
 #include <vector>
 #include <functional> // For std::function
 #include <cctype>     // For std::isalnum
+#include <cmath>      // For std::ceil
 
 // Check if GTK is enabled in build
 #ifdef LUMACS_WITH_GTK
@@ -96,6 +97,20 @@ void GtkEditor::handle_editor_event(EditorEvent event) {
             queue_redraw_all_windows(content_widget_);
         }
         return;
+    } else if (event == EditorEvent::FontChanged) {
+        // Update renderer with new font size from config
+        if (gtk_renderer_ && core_) {
+            gtk_renderer_->set_font_size(core_->font_size());
+            // Update minibuffer height to match new font metrics
+            if (minibuffer_drawing_area_) {
+                int minibuffer_height = static_cast<int>(std::ceil(gtk_renderer_->line_height() + 16.0));
+                minibuffer_drawing_area_->set_content_height(minibuffer_height);
+            }
+        }
+        if (content_widget_) {
+            queue_redraw_all_windows(content_widget_);
+        }
+        return;
     } else if (event == EditorEvent::TransientMessageCleared) {
         if (content_widget_) queue_redraw_all_windows(content_widget_);
         return;
@@ -463,11 +478,16 @@ void GtkEditor::rebuild_layout() {
 
     // Create Minibuffer DrawingArea
     minibuffer_drawing_area_ = Gtk::make_managed<Gtk::DrawingArea>();
-    // Approximate height for minibuffer (line height + padding). 
-    // We don't have exact metrics here yet, but 40px is safe for Monospace 12.
-    minibuffer_drawing_area_->set_content_height(40); 
+    // Calculate proper minibuffer height from font metrics if renderer is available
+    // Otherwise use a safe default that will be updated when renderer initializes
+    int minibuffer_height = 40; // Default fallback
+    if (gtk_renderer_) {
+        // Use actual line height plus padding for proper sizing
+        minibuffer_height = static_cast<int>(std::ceil(gtk_renderer_->line_height() + 16.0)); // +16 for padding
+    }
+    minibuffer_drawing_area_->set_content_height(minibuffer_height);
     minibuffer_drawing_area_->set_expand(false);
-    
+
     minibuffer_drawing_area_->set_draw_func([this](const Cairo::RefPtr<Cairo::Context>& cr, int width, int height) {
         if (gtk_renderer_) {
             gtk_renderer_->render_minibuffer(cr, width, height);

+ 248 - 121
src/gtk_renderer.cpp

@@ -3,39 +3,73 @@
 #include "lumacs/minibuffer_manager.hpp" // For MinibufferManager and MinibufferMode
 #include "lumacs/keybinding.hpp" // For KeyBindingManager
 #include "lumacs/message_formatter.hpp"
-#include <algorithm> // For std::max, std::min
+#include <algorithm> // For std::max, std::min, std::round
+#include <cmath> // For std::ceil, std::floor, std::round
 #include <spdlog/spdlog.h> // Added for debug logging
+#include <glibmm/ustring.h> // For UTF-8 handling
 
 namespace lumacs {
 
 GtkRenderer::GtkRenderer(EditorCore& core, Gtk::Widget& context_widget)
     : core_(core), context_widget_(context_widget) {
+    // Get font size from config
+    font_size_ = core_.config().get<int>("font_size", 12);
     initialize_font_metrics(); // Initialize font metrics once during construction
 }
 
 void GtkRenderer::initialize_font_metrics() {
-    if (font_initialized_) return;
-
     // Use a minimal string for layout creation
     auto layout = context_widget_.create_pango_layout(" ");
-    font_desc_ = Pango::FontDescription("Monospace 12");
+    font_desc_ = Pango::FontDescription("Monospace " + std::to_string(font_size_));
     layout->set_font_description(font_desc_);
-    
-    Pango::FontMetrics metrics = layout->get_context()->get_metrics(font_desc_);
-    line_height_ = (double)metrics.get_height() / PANGO_SCALE;
-    ascent_ = (double)metrics.get_ascent() / PANGO_SCALE;
-    
+
+    // Configure font options for crisp text rendering
+    auto pango_context = layout->get_context();
+    Cairo::FontOptions font_options;
+    font_options.set_antialias(Cairo::ANTIALIAS_SUBPIXEL);
+    font_options.set_hint_style(Cairo::FontOptions::HintStyle::MEDIUM);
+    font_options.set_hint_metrics(Cairo::FontOptions::HintMetrics::ON);
+    font_options.set_subpixel_order(Cairo::SUBPIXEL_ORDER_RGB);
+    pango_context->set_cairo_font_options(font_options);
+
+    Pango::FontMetrics metrics = pango_context->get_metrics(font_desc_);
+
+    // Calculate line height with proper spacing - use ascent + descent + small padding
+    double ascent = static_cast<double>(metrics.get_ascent()) / PANGO_SCALE;
+    double descent = static_cast<double>(metrics.get_descent()) / PANGO_SCALE;
+
+    // Round up line height to avoid fractional pixel issues
+    line_height_ = std::ceil(ascent + descent + 2.0); // +2 for minimal line spacing
+    ascent_ = ascent;
+    descent_ = descent;
+
     // Get character width more precisely using index_to_pos for a known monospace char
     layout->set_text("M"); // Use a single, representative character
     Pango::Rectangle pos0 = layout->index_to_pos(0); // Position at index 0 (before 'M')
     Pango::Rectangle pos1 = layout->index_to_pos(1); // Position at index 1 (after 'M')
-    char_width_ = (double)(pos1.get_x() - pos0.get_x()) / PANGO_SCALE;
+    char_width_ = static_cast<double>(pos1.get_x() - pos0.get_x()) / PANGO_SCALE;
+
+    // Get HiDPI scale factor
+    scale_factor_ = context_widget_.get_scale_factor();
+
+    spdlog::debug("GtkRenderer font metrics: line_height={}, ascent={}, descent={}, char_width={}, scale={}",
+                  line_height_, ascent_, descent_, char_width_, scale_factor_);
 
-    spdlog::debug("GtkRenderer font metrics: line_height={}, char_width={}", line_height_, char_width_);
-    
     font_initialized_ = true;
 }
 
+size_t GtkRenderer::char_to_byte_index(const std::string& str, size_t char_pos) {
+    // Convert character position to byte index for UTF-8 string
+    Glib::ustring ustr(str);
+    if (char_pos >= ustr.length()) {
+        return str.length();
+    }
+    // Get the byte offset for the character at char_pos
+    auto it = ustr.begin();
+    std::advance(it, char_pos);
+    return it.base() - ustr.begin().base();
+}
+
 void GtkRenderer::apply_face_attributes(Pango::AttrList& attr_list, const FaceAttributes& face, int start_index, int end_index) {
     if (start_index >= end_index) return;
 
@@ -166,13 +200,21 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
     int content_width_px = width - static_cast<int>(PADDING_LEFT + PADDING_RIGHT);
     int content_height_px = height - static_cast<int>(PADDING_TOP + PADDING_BOTTOM);
 
-    int visible_lines = static_cast<int>(content_height_px / line_height_);
-    int visible_cols = static_cast<int>(content_width_px / char_width_);
+    // Use floor for visible lines/cols to avoid rendering partial lines
+    int visible_lines = static_cast<int>(std::floor(content_height_px / line_height_));
+    int visible_cols = static_cast<int>(std::floor(content_width_px / char_width_));
 
     // Reserve space for modeline (1 line). Minibuffer is now separate.
     int editor_lines = std::max(0, visible_lines - 1);
     window->set_viewport_size(visible_cols, editor_lines);
 
+    // Visual line mode (soft word wrap)
+    bool visual_line_mode = core_.visual_line_mode();
+    if (visual_line_mode) {
+        layout->set_width(content_width_px * PANGO_SCALE);
+        layout->set_wrap(Pango::WrapMode::WORD_CHAR);
+    }
+
     // Region/Mark Calculation
     std::optional<Range> selection_range;
     if (buffer.has_active_mark() && buffer.mark()) {
@@ -185,28 +227,53 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
 
         // Render visible lines
         auto [start_line, end_line] = window->visible_line_range();
-        int horizontal_offset = window->viewport().horizontal_offset;
-        
+        int horizontal_offset = visual_line_mode ? 0 : window->viewport().horizontal_offset;
+
         auto& window_cache = render_cache_[window.get()];
-    
-        for (int screen_y = 0; screen_y < editor_lines && start_line + screen_y < end_line; ++screen_y) {
-            size_t buffer_line_idx = start_line + screen_y;
-            if (buffer_line_idx >= buffer.line_count()) break; // Safety check
+
+        // In visual line mode, we need to track screen rows consumed
+        int current_screen_row = 0;
+        size_t buffer_line_idx = start_line;
+
+        while (current_screen_row < editor_lines && buffer_line_idx < buffer.line_count()) {
+            // Legacy loop condition for non-wrapped mode
+            if (!visual_line_mode && buffer_line_idx >= end_line) break;
+
             const auto& line_text = buffer.line(buffer_line_idx);
             const auto& styles = buffer.get_line_styles(buffer_line_idx);
-    
-            // Apply horizontal scrolling
+
+            // Apply horizontal scrolling (disabled in visual line mode)
             std::string visible_text;
             if (horizontal_offset < static_cast<int>(line_text.length())) {
                 visible_text = line_text.substr(horizontal_offset);
+            } else if (visual_line_mode) {
+                visible_text = line_text;  // Show full line in visual mode
             }
-            
+
             // Always set text for the layout before any further operations, especially cursor positioning
             layout->set_text(visible_text);
-            
-            double text_x = PADDING_LEFT;
-            double text_y = PADDING_TOP + screen_y * line_height_;
-    
+
+            // Calculate how many screen rows this line takes (for visual line mode)
+            int line_screen_rows = 1;
+            if (visual_line_mode && !visible_text.empty()) {
+                int layout_height = 0;
+                layout->get_pixel_size(layout_height, layout_height);
+                auto iter = layout->get_iter();
+                int line_count = 0;
+                do {
+                    line_count++;
+                } while (iter.next_line());
+                line_screen_rows = std::max(1, line_count);
+            }
+
+            // Calculate pixel-aligned positions to avoid blurry text
+            // Round to whole pixels to prevent subpixel rendering artifacts
+            double text_x = std::round(PADDING_LEFT);
+            double text_y = std::round(PADDING_TOP + current_screen_row * line_height_);
+
+            // For wrapped lines, we need the full layout height
+            double effective_line_height = visual_line_mode ? (line_screen_rows * line_height_) : line_height_;
+
             // Check for Region/Selection Intersection
             bool has_selection = false;
             int sel_start = 0, sel_end = 0;
@@ -230,9 +297,9 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
             bool use_cache = false;
             if (!has_selection && window_cache.count(buffer_line_idx)) {
                  auto& entry = window_cache[buffer_line_idx];
-                 // Validate both text content AND styles to ensure cache is still valid
-                 // (styles change on theme switch or syntax highlighting updates)
-                 if (entry.text_content == visible_text && entry.styles == styles) {
+                 // Validate text content, styles, AND scale factor to ensure cache is still valid
+                 // (styles change on theme switch, scale changes on display switch)
+                 if (entry.text_content == visible_text && entry.styles == styles && entry.scale == scale_factor_) {
                      cr->set_source(entry.surface, text_x, text_y);
                      cr->paint();
                      use_cache = true;
@@ -274,37 +341,80 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
                 layout->set_attributes(attr_list);
     
                 // Render
-                if (!has_selection) {
-                    // Render to Cache surface first
-                                int surf_width = std::max(1, static_cast<int>(content_width_px));
-                                
-                                auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, surf_width, static_cast<int>(line_height_));                    auto cr_surf = Cairo::Context::create(surface);
-                    
+                if (!has_selection && !visual_line_mode) {
+                    // Render to Cache surface first (disabled in visual line mode for simplicity)
+                    // Create HiDPI-aware surface
+                    int surf_width = std::max(1, static_cast<int>(std::ceil(content_width_px * scale_factor_)));
+                    int surf_height = static_cast<int>(std::ceil(effective_line_height * scale_factor_));
+
+                    auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, surf_width, surf_height);
+                    // Set device scale for HiDPI - this makes Cairo render at higher resolution
+                    cairo_surface_set_device_scale(surface->cobj(), scale_factor_, scale_factor_);
+
+                    auto cr_surf = Cairo::Context::create(surface);
+
                     // Clear surface
                     cr_surf->set_operator(Cairo::Context::Operator::CLEAR);
                     cr_surf->paint();
                     cr_surf->set_operator(Cairo::Context::Operator::OVER);
-                    
+
                     cr_surf->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
                     layout->show_in_cairo_context(cr_surf); // Render layout to surface
-    
-                    // Store
+
+                    // Store with scale factor for cache validation
                     LineCacheEntry entry;
                     entry.text_content = visible_text;
                     entry.surface = surface;
                     entry.styles = styles;
+                    entry.scale = scale_factor_;
                     window_cache[buffer_line_idx] = entry;
-    
+
                     // Blit to screen
                     cr->set_source(surface, text_x, text_y);
                     cr->paint();
-    
+
                 } else {
-                    // Render directly to screen (don't cache selected text)
+                    // Render directly to screen (don't cache selected text or wrapped lines)
                     cr->move_to(text_x, text_y);
                     cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
                     layout->show_in_cairo_context(cr);
                 }
+            } else if (visual_line_mode) {
+                // In visual line mode, always render directly (simpler for wrapped text)
+                Pango::AttrList attr_list;
+
+                // Apply Syntax Highlighting
+                for (const auto& style : styles) {
+                    if (auto face = theme->get_face(style.attr.face_name)) {
+                        int start = static_cast<int>(style.range.start.column);
+                        int end = static_cast<int>(style.range.end.column);
+
+                        start = std::max(0, start);
+                        end = std::min(static_cast<int>(visible_text.length()), end);
+
+                        if (start < end) {
+                            apply_face_attributes(attr_list, *face, start, end);
+                        }
+                    }
+                }
+
+                // Apply selection if any
+                if (has_selection) {
+                    if (auto region_face = theme->get_face("region")) {
+                        apply_face_attributes(attr_list, *region_face, sel_start, sel_end);
+                    } else {
+                        auto attr = Pango::Attribute::create_attr_background(0x4444, 0x4444, 0x4444);
+                        attr.set_start_index(sel_start);
+                        attr.set_end_index(sel_end);
+                        attr_list.insert(attr);
+                    }
+                }
+
+                layout->set_attributes(attr_list);
+
+                cr->move_to(text_x, text_y);
+                cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
+                layout->show_in_cairo_context(cr);
             }
     
             // Render Cursor (assuming `cursor_visible_` is managed by GtkEditor)
@@ -368,13 +478,11 @@ void GtkRenderer::draw_window(const Cairo::RefPtr<Cairo::Context>& cr, int width
                      }
                  }
             }
-        }    
-    // Refined Loop with Selection Logic:
-    // (This replaces the loop in the file)
-    
-    // Since I cannot easily rewrite the whole loop with complex logic insertion in one go without potentially breaking,
-    // I will rewrite the loop logic completely in the replacement string.
 
+            // Advance to next line
+            buffer_line_idx++;
+            current_screen_row += line_screen_rows;
+        }
 
 } // Closing brace for GtkRenderer::draw_window method
 
@@ -386,43 +494,44 @@ void GtkRenderer::render_modeline_for_window(const Cairo::RefPtr<Cairo::Context>
 
     // The logic for is_active needs to be passed from GtkEditor
     bool is_active = (window == active_window_cache);
-    
+
     // Calculate modeline position (bottom of the drawing area)
     // Minibuffer is separate, so modeline is at the very bottom
-    double modeline_y = height - line_height_ - PADDING_BOTTOM; 
-    double modeline_x = PADDING_LEFT;
-    
+    // Use pixel-aligned coordinates to avoid blurry text
+    double modeline_y = std::round(height - line_height_ - PADDING_BOTTOM);
+    double modeline_x = std::round(PADDING_LEFT);
+
     // Get theme colors
     auto theme = core_.theme_manager().active_theme();
     ThemeElement element = is_active ? ThemeElement::StatusLine : ThemeElement::StatusLineInactive;
-    
+
     Color bg = theme->get_bg_color(element);
     Color fg = theme->get_fg_color(element);
-    
+
     // Draw modeline background
     cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
     cr->rectangle(0, modeline_y, width, line_height_);
     cr->fill();
-    
+
     // Build modeline content using ModelineManager
     auto content = core_.modeline_manager().generate_content(window, is_active);
-    
+
     double x_offset = modeline_x;
     for (const auto& chunk : content) {
         Color chunk_fg = fg;
-        
+
         // Resolve chunk face if needed
         if (!chunk.face_name.empty()) {
              if (auto face = theme->get_face(chunk.face_name)) {
                  if (face->foreground) chunk_fg = *face->foreground;
              }
         }
-        
+
         // Use a temporary layout for modeline chunk
         auto layout = Pango::Layout::create(context_widget_.get_pango_context());
         layout->set_font_description(font_desc_);
         layout->set_text(chunk.text);
-        
+
         // Apply attributes
         Pango::AttrList attr_list;
         if (!chunk.face_name.empty()) {
@@ -435,11 +544,11 @@ void GtkRenderer::render_modeline_for_window(const Cairo::RefPtr<Cairo::Context>
         cr->set_source_rgb(chunk_fg.r / 255.0, chunk_fg.g / 255.0, chunk_fg.b / 255.0);
         cr->move_to(x_offset, modeline_y);
         layout->show_in_cairo_context(cr);
-        
-        // Advance
+
+        // Advance - keep pixel aligned
         int w, h;
         layout->get_pixel_size(w, h);
-        x_offset += w;
+        x_offset = std::round(x_offset + w);
     }
 }
 
@@ -485,22 +594,23 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
 
     // If showing extended echo area lines, render them first (above the main minibuffer line)
     if (showing_echo_lines) {
-        double hint_y = PADDING_TOP;
+        double hint_y = std::round(PADDING_TOP);
         cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
 
         for (const auto& line : echo_area_lines) {
             auto layout = Pango::Layout::create(context_widget_.get_pango_context());
             layout->set_font_description(font_desc_);
             layout->set_text(line);
-            cr->move_to(PADDING_LEFT, hint_y);
+            cr->move_to(std::round(PADDING_LEFT), hint_y);
             layout->show_in_cairo_context(cr);
-            hint_y += line_height_;
+            hint_y = std::round(hint_y + line_height_);
         }
     }
 
     // Calculate minibuffer position (bottom line with padding)
-    double minibuffer_y = height - line_height_ - PADDING_BOTTOM;
-    double minibuffer_x = PADDING_LEFT;
+    // Use pixel-aligned coordinates to avoid blurry text
+    double minibuffer_y = std::round(height - line_height_ - PADDING_BOTTOM);
+    double minibuffer_x = std::round(PADDING_LEFT);
 
     // Prepare minibuffer text
     std::string minibuffer_text;
@@ -587,78 +697,81 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
         layout->show_in_cairo_context(cr);
     }
     
-    // Render minibuffer cursor if active and visible (assuming cursor_visible is managed by GtkEditor)
-    // bool cursor_visible_ = true; // Placeholder - in real usage passed or managed
-    if (core_.minibuffer_manager().is_active()) { // Assuming always visible or managed externally
-        // Calculate cursor position in minibuffer
-        // text is prompt + input. Cursor is relative to input start.
-        size_t cursor_idx = prompt_part.length() + core_.minibuffer_manager().get_cursor_position();
-        
+    // Render minibuffer cursor if active and visible
+    if (core_.minibuffer_manager().is_active()) {
+        // Calculate cursor position in minibuffer using proper UTF-8 byte indexing
+        // Cursor position from minibuffer is in characters, we need byte index for Pango
+        size_t cursor_char_pos = core_.minibuffer_manager().get_cursor_position();
+
+        // Convert character positions to byte indices for UTF-8 strings
+        // prompt_part byte length + byte offset within input_part for cursor position
+        size_t prompt_byte_len = prompt_part.length(); // Prompt is typically ASCII, bytes == chars
+        size_t cursor_byte_idx = prompt_byte_len + char_to_byte_index(input_part, cursor_char_pos);
+
         auto layout = Pango::Layout::create(context_widget_.get_pango_context());
         layout->set_font_description(font_desc_);
-        layout->set_text(minibuffer_text); 
-        
-        Pango::Rectangle pos = layout->index_to_pos(static_cast<int>(cursor_idx));
-        double cursor_screen_x = minibuffer_x + (pos.get_x() / (double)PANGO_SCALE);
-        
-        // Draw Cursor Block
-        // Use char_width_ as default, or measure actual char
+        layout->set_text(minibuffer_text);
+
+        Pango::Rectangle pos = layout->index_to_pos(static_cast<int>(cursor_byte_idx));
+        double cursor_screen_x = std::round(minibuffer_x + (pos.get_x() / static_cast<double>(PANGO_SCALE)));
+
+        // Draw Cursor Block - measure actual char width using Pango
         double cur_width = char_width_;
-        // If not at end, measure actual char width
-        if (cursor_idx < minibuffer_text.length()) {
-             Pango::Rectangle next_pos = layout->index_to_pos(static_cast<int>(cursor_idx + 1));
-             cur_width = (next_pos.get_x() - pos.get_x()) / (double)PANGO_SCALE;
+        size_t text_byte_len = minibuffer_text.length();
+        if (cursor_byte_idx < text_byte_len) {
+            // Get next character's byte index for width calculation
+            Glib::ustring utext(minibuffer_text);
+            size_t total_char_pos = Glib::ustring(prompt_part).length() + cursor_char_pos;
+            if (total_char_pos < utext.length()) {
+                size_t next_byte_idx = char_to_byte_index(minibuffer_text, total_char_pos + 1);
+                Pango::Rectangle next_pos = layout->index_to_pos(static_cast<int>(next_byte_idx));
+                cur_width = (next_pos.get_x() - pos.get_x()) / static_cast<double>(PANGO_SCALE);
+            }
         }
 
         cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
         cr->rectangle(cursor_screen_x, minibuffer_y, cur_width, line_height_);
         cr->fill();
-        
+
         // Draw Character Inverted (if not at end)
-        if (cursor_idx < minibuffer_text.length()) {
-             // Extract char at cursor
-             // Note: simple indexing works for ASCII, but for UTF-8 need careful extraction.
-             // Pango layout handles rendering, we just need to render that one char at the pos.
-             // Simpler: Set color to BG and re-render the layout clipped? 
-             // Or just render single char?
-             // Let's reuse the main editor approach: create layout for single char.
-             // But we need the exact byte string for the character at cursor_idx (utf8 aware).
-             
-             // For now, let's rely on std::string indexing assuming ASCII for command/paths mostly, 
-             // or use Pango to iterate?
-             // Glib::ustring is better for UTF8.
-             // Let's assume standard char for now to fix the main visual bug.
-             char cursor_char = minibuffer_text[cursor_idx];
-             
-             cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
-             auto cursor_layout = context_widget_.create_pango_layout(std::string(1, cursor_char));
-             cursor_layout->set_font_description(font_desc_);
-             cr->move_to(cursor_screen_x, minibuffer_y);
-             cursor_layout->show_in_cairo_context(cr);
+        if (cursor_byte_idx < text_byte_len) {
+            // Extract the UTF-8 character at cursor position using Glib::ustring
+            Glib::ustring utext(minibuffer_text);
+            size_t total_char_pos = Glib::ustring(prompt_part).length() + cursor_char_pos;
+            if (total_char_pos < utext.length()) {
+                // Get single character as string
+                Glib::ustring cursor_char_str = utext.substr(total_char_pos, 1);
+
+                cr->set_source_rgb(bg.r / 255.0, bg.g / 255.0, bg.b / 255.0);
+                auto cursor_layout = context_widget_.create_pango_layout(cursor_char_str);
+                cursor_layout->set_font_description(font_desc_);
+                cr->move_to(cursor_screen_x, minibuffer_y);
+                cursor_layout->show_in_cairo_context(cr);
+            }
         }
 
         // Render completion overlay if applicable
         auto current_completion = core_.minibuffer_manager().get_current_completion();
         if (current_completion && input_part != *current_completion) {
-            // Safety check: ensure input_part length is valid for substring extraction
-            if (input_part.length() <= current_completion->length()) {
-                std::string completion_suffix = current_completion->substr(input_part.length());
+            // Use character-based length comparison for proper UTF-8 handling
+            Glib::ustring u_input(input_part);
+            Glib::ustring u_completion(*current_completion);
+            if (u_input.length() <= u_completion.length()) {
+                Glib::ustring completion_suffix = u_completion.substr(u_input.length());
                 if (!completion_suffix.empty()) {
-                    // Position completion at the end of the input text (cursor might be in middle)
-                    // completion suffix appends to the END of input.
-                    
-                    // Calculate position of the end of input
-                    size_t input_end_idx = prompt_part.length() + input_part.length();
-                    Pango::Rectangle end_pos = layout->index_to_pos(static_cast<int>(input_end_idx));
-                    double completion_x = minibuffer_x + (end_pos.get_x() / (double)PANGO_SCALE);
+                    // Calculate position of the end of input using byte index
+                    size_t input_end_byte_idx = prompt_byte_len + input_part.length();
+                    Pango::Rectangle end_pos = layout->index_to_pos(static_cast<int>(input_end_byte_idx));
+                    double completion_x = std::round(minibuffer_x + (end_pos.get_x() / static_cast<double>(PANGO_SCALE)));
 
                     auto completion_layout = Pango::Layout::create(context_widget_.get_pango_context());
                     completion_layout->set_font_description(font_desc_);
-                    completion_layout->set_text(completion_suffix); // Only draw suffix
-                    
+                    completion_layout->set_text(completion_suffix);
+
                     Pango::AttrList completion_attr_list;
+                    std::string suffix_str = completion_suffix;
                     if (auto face = theme->get_face("minibuffer-completion")) {
-                        apply_face_attributes(completion_attr_list, *face, 0, static_cast<int>(completion_suffix.length()));
+                        apply_face_attributes(completion_attr_list, *face, 0, static_cast<int>(suffix_str.length()));
                     } else {
                         // Fallback: dimmed foreground
                         Color dim_fg = fg;
@@ -667,13 +780,13 @@ void GtkRenderer::render_minibuffer(const Cairo::RefPtr<Cairo::Context>& cr, int
                         dim_fg.b = static_cast<unsigned char>(dim_fg.b * 0.7);
                         auto attr = Pango::Attribute::create_attr_foreground(dim_fg.r * 257, dim_fg.g * 257, dim_fg.b * 257);
                         attr.set_start_index(0);
-                        attr.set_end_index(static_cast<int>(completion_suffix.length()));
+                        attr.set_end_index(static_cast<int>(suffix_str.length()));
                         completion_attr_list.insert(attr);
                     }
                     completion_layout->set_attributes(completion_attr_list);
 
                     cr->set_source_rgb(fg.r / 255.0, fg.g / 255.0, fg.b / 255.0);
-                    cr->move_to(completion_x, minibuffer_y); 
+                    cr->move_to(completion_x, minibuffer_y);
                     completion_layout->show_in_cairo_context(cr);
                 }
             }
@@ -697,4 +810,18 @@ void GtkRenderer::invalidate_cache() {
     spdlog::debug("GtkRenderer: Cache invalidated due to theme change.");
 }
 
+void GtkRenderer::set_font_size(int size) {
+    if (size < 6) size = 6;    // Minimum readable size
+    if (size > 72) size = 72;  // Maximum reasonable size
+
+    font_size_ = size;
+    core_.config().set("font_size", size);
+
+    // Reinitialize font metrics
+    initialize_font_metrics();
+    invalidate_cache();
+
+    spdlog::debug("GtkRenderer: Font size changed to {}", size);
+}
+
 } // namespace lumacs

+ 2 - 2
src/keybinding.cpp

@@ -257,9 +257,9 @@ KeyBinding::KeyBinding(const std::string& key_str, std::string cmd_name, const s
 // KeyBindingManager Implementation
 // ============================================================================ 
 
-KeyBindingManager::KeyBindingManager(CommandSystem* command_system) 
+KeyBindingManager::KeyBindingManager(CommandSystem* command_system)
     : root_(std::make_unique<TrieNode>()), // Initialize root_
-      sequence_timeout_(std::chrono::milliseconds(1000)), 
+      sequence_timeout_(std::chrono::milliseconds(5000)),  // 5 seconds - enough for which-key
       command_system_(command_system) {
 }
 

+ 22 - 0
src/lua_api.cpp

@@ -813,6 +813,28 @@ void LuaApi::register_types() {
         "has_prefix_bindings", [](EditorCore& core, const std::string& prefix) {
             return core.keybinding_manager().has_prefix_bindings(KeySequence(prefix));
         },
+        "set_sequence_timeout", [](EditorCore& core, int ms) {
+            core.keybinding_manager().set_sequence_timeout(std::chrono::milliseconds(ms));
+        },
+        "get_sequence_timeout", [](EditorCore& core) {
+            return static_cast<int>(core.keybinding_manager().get_sequence_timeout().count());
+        },
+
+        // Font size
+        "set_font_size", [](EditorCore& core, int size) {
+            core.set_font_size(size);
+        },
+        "font_size", [](EditorCore& core) {
+            return core.font_size();
+        },
+
+        // Visual line mode (soft word wrap)
+        "set_visual_line_mode", [](EditorCore& core, bool enabled) {
+            core.set_visual_line_mode(enabled);
+        },
+        "visual_line_mode", [](EditorCore& core) {
+            return core.visual_line_mode();
+        },
 
         // Extended echo area (for packages like which-key)
         "set_echo_area", [](EditorCore& core, sol::table lines_table) {

+ 4 - 1
src/mode_activator.cpp

@@ -99,9 +99,12 @@ void ModeActivator::activate_find_file_mode() {
         [this](const std::string& input) {
             if (core_.load_file(input)) {
                 core_.set_message("Loaded: " + input);
-                // Trigger major mode auto-activation
+                // Trigger major mode auto-activation and recentf tracking
                 if (core_.lua_api()) {
                     core_.lua_api()->execute("auto_activate_major_mode()");
+                    // Track in recent files if recentf is loaded
+                    core_.lua_api()->execute(
+                        "if lumacs.recentf_track then lumacs.recentf_track('" + input + "') end");
                 }
             } else {
                 core_.set_message("Failed to load: " + input);