-- Lumacs Configuration File -- This file is executed on startup and allows you to customize keybindings, -- create commands, and extend the editor with Lua. print("Loading init.lua...") -- ============================================================================ -- MODE SYSTEM (Emacs-style Major and Minor Modes) -- ============================================================================ -- Mode registries local major_modes = {} local minor_modes = {} -- Active modes per buffer (keyed by buffer name) local buffer_major_modes = {} local buffer_minor_modes = {} -- Define a major mode function define_major_mode(name, config) major_modes[name] = { name = name, file_patterns = config.file_patterns or {}, setup = config.setup or function() end, cleanup = config.cleanup or function() end, highlight = config.highlight or nil, keybindings = config.keybindings or {}, comment_syntax = config.comment_syntax or "--", } print(string.format("[Mode] Registered major mode: %s", name)) end -- Define a minor mode function define_minor_mode(name, config) minor_modes[name] = { name = name, setup = config.setup or function() end, cleanup = config.cleanup or function() end, keybindings = config.keybindings or {}, global = config.global or false, -- If true, applies to all buffers } print(string.format("[Mode] Registered minor mode: %s", name)) end -- Activate a major mode for the current buffer function activate_major_mode(mode_name) local buf = editor.buffer local buf_name = buf:name() local mode = major_modes[mode_name] if not mode then message("Unknown major mode: " .. mode_name) return false end -- Deactivate current major mode if any if buffer_major_modes[buf_name] then deactivate_major_mode() end print(string.format("[Mode] Activating major mode '%s' for buffer '%s'", mode_name, buf_name)) -- Store active mode buffer_major_modes[buf_name] = mode_name -- Set up event handler for auto-highlighting if mode.highlight then buf:on_buffer_event(function(event_data) local current_buf = editor.buffer if event_data.event == lumacs.BufferEvent.Loaded or event_data.event == lumacs.BufferEvent.LanguageChanged then mode.highlight() print(string.format("[Mode] Auto-highlighted buffer with %s", mode_name)) end end) -- Highlight immediately mode.highlight() end -- Apply mode-specific keybindings (these are temporary for this buffer) for key, func in pairs(mode.keybindings) do bind_key(key, func) end -- Run setup function mode.setup() message(string.format("Major mode: %s", mode_name)) return true end -- Deactivate current major mode function deactivate_major_mode() local buf = editor.buffer local buf_name = buf:name() local mode_name = buffer_major_modes[buf_name] if not mode_name then return end local mode = major_modes[mode_name] if mode and mode.cleanup then mode.cleanup() end buffer_major_modes[buf_name] = nil print(string.format("[Mode] Deactivated major mode '%s'", mode_name)) end -- Toggle a minor mode function toggle_minor_mode(mode_name) local buf = editor.buffer local buf_name = buf:name() local mode = minor_modes[mode_name] if not mode then message("Unknown minor mode: " .. mode_name) return end -- Initialize minor modes table for this buffer if not buffer_minor_modes[buf_name] then buffer_minor_modes[buf_name] = {} end local is_active = buffer_minor_modes[buf_name][mode_name] if is_active then -- Deactivate if mode.cleanup then mode.cleanup() end buffer_minor_modes[buf_name][mode_name] = nil message(string.format("Minor mode disabled: %s", mode_name)) else -- Activate buffer_minor_modes[buf_name][mode_name] = true if mode.setup then mode.setup() end message(string.format("Minor mode enabled: %s", mode_name)) end end -- Auto-detect and activate major mode based on file extension function auto_activate_major_mode() local buf = editor.buffer local buf_name = buf:name() -- Try to match file pattern for mode_name, mode in pairs(major_modes) do for _, pattern in ipairs(mode.file_patterns) do if string.match(buf_name, pattern) then activate_major_mode(mode_name) return end end end -- No match, use fundamental mode (default) print(string.format("[Mode] No major mode matched for '%s', using fundamental-mode", buf_name)) end -- Get current major mode name function current_major_mode() local buf = editor.buffer local buf_name = buf:name() return buffer_major_modes[buf_name] or "fundamental-mode" end -- ============================================================================ -- MAJOR MODES -- ============================================================================ -- Lua Mode define_major_mode("lua-mode", { file_patterns = {"%.lua$"}, comment_syntax = "--", highlight = function() local buf = editor.buffer buf:clear_styles() -- Keywords to highlight local keywords = { "function", "local", "end", "if", "then", "else", "elseif", "for", "while", "do", "return", "break", "and", "or", "not", "true", "false", "nil", "in", "repeat", "until" } -- Highlight each line for line_num = 0, buf:line_count() - 1 do local line_text = buf:line(line_num) -- Highlight keywords for _, keyword in ipairs(keywords) do local start_pos = 1 while true do local pattern = "%f[%w]" .. keyword .. "%f[%W]" local pos = string.find(line_text, pattern, start_pos) if not pos then break end local range = lumacs.Range( lumacs.Position(line_num, pos - 1), lumacs.Position(line_num, pos + #keyword - 1) ) buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Keyword, 0)) start_pos = pos + #keyword end end -- Highlight strings local start_pos = 1 while true do local quote_start = string.find(line_text, '"', start_pos, true) if not quote_start then break end local quote_end = string.find(line_text, '"', quote_start + 1, true) if not quote_end then break end local range = lumacs.Range( lumacs.Position(line_num, quote_start - 1), lumacs.Position(line_num, quote_end) ) buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.String, 0)) start_pos = quote_end + 1 end -- Highlight comments local comment_pos = string.find(line_text, "--", 1, true) if comment_pos then local range = lumacs.Range( lumacs.Position(line_num, comment_pos - 1), lumacs.Position(line_num, #line_text) ) buf:set_style(range, lumacs.TextAttribute(lumacs.ColorType.Comment, 0)) end end end, setup = function() print("[lua-mode] Lua mode activated") end, cleanup = function() print("[lua-mode] Lua mode deactivated") end, keybindings = { -- Lua-specific keybindings can go here } }) -- Fundamental Mode (default/fallback) define_major_mode("fundamental-mode", { file_patterns = {}, setup = function() print("[fundamental-mode] Fundamental mode activated") end }) -- ============================================================================ -- MINOR MODES -- ============================================================================ -- Auto-save minor mode define_minor_mode("auto-save-mode", { global = false, setup = function() -- TODO: Set up auto-save timer print("[auto-save-mode] Auto-save enabled") end, cleanup = function() print("[auto-save-mode] Auto-save disabled") end }) -- Line numbers minor mode (conceptual - already always shown) define_minor_mode("line-numbers-mode", { global = true, setup = function() print("[line-numbers-mode] Line numbers enabled") end, cleanup = function() print("[line-numbers-mode] Line numbers disabled") end }) -- ============================================================================ -- GLOBAL KEYBINDINGS -- ============================================================================ -- Example: Custom keybindings -- Syntax: bind_key("key", function() ... end) -- Emacs-style navigation (Ctrl+N/P for next/previous line) bind_key("C-n", function() editor:move_down() message("Moved down") end) bind_key("C-p", function() editor:move_up() message("Moved up") end) bind_key("C-f", function() editor:move_right() end) bind_key("C-b", function() editor:move_left() end) -- Emacs-style line navigation bind_key("C-a", function() editor:move_to_line_start() end) bind_key("C-e", function() editor:move_to_line_end() end) -- M-f (forward-word) - Move forward one word bind_key("M-f", function() editor:move_forward_word() end) -- M-b (backward-word) - Move backward one word bind_key("M-b", function() editor:move_backward_word() end) -- C-v (scroll-up) - Page down bind_key("C-v", function() editor:page_down() end) -- M-v (scroll-down) - Page up bind_key("M-v", function() editor:page_up() end) -- M-< (beginning-of-buffer) - Go to start bind_key("M-<", function() editor:goto_beginning() message("Beginning of buffer") end) -- M-> (end-of-buffer) - Go to end bind_key("M->", function() editor:goto_end() message("End of buffer") end) -- M-g M-g (goto-line) - Jump to line number bind_key("M-g g", function() editor:command_mode() -- TODO: Implement line number input in command mode end) -- Note: C-s binding moved to avoid conflicts with isearch -- Custom command: Insert timestamp bind_key("C-t", function() local cursor_pos = editor.cursor local timestamp = os.date("%Y-%m-%d %H:%M:%S") editor.buffer:insert(cursor_pos, timestamp) message("Inserted timestamp") end) -- Example: Helper functions you can define function goto_line(line_num) local pos = lumacs.Position(line_num - 1, 0) editor:set_cursor(pos) message("Jumped to line " .. line_num) end -- Example: Buffer inspection function buffer_info() local buf = editor.buffer local cursor = editor.cursor message(string.format( "Buffer: %s | Lines: %d | Cursor: %d,%d | Modified: %s", buf:name(), buf:line_count(), cursor.line + 1, cursor.column + 1, buf:is_modified() and "yes" or "no" )) end -- Bind to show buffer info -- Note: C-i and Tab are indistinguishable in most terminals (both send ASCII 9) -- Using C-u instead for buffer info bind_key("C-u", buffer_info) -- Search helper function function find_next(query) local buf = editor.buffer local cursor = editor.cursor -- Start searching AFTER the current cursor position to find the next occurrence -- Otherwise we might find the same one if we are sitting on it. -- A simple way is to advance column by 1 for the search start. local search_start = lumacs.Position(cursor.line, cursor.column + 1) -- If at end of line, search from start of next line is handled by find() implementation? -- Buffer::find currently implements simple linear search from a position. -- If column is beyond end, it should handle it. Let's trust the C++ impl or adjust. local res = buf:find(query, search_start) if res then editor.cursor = res.start message("Found '" .. query .. "' at " .. res.start.line .. ":" .. res.start.column) -- Optional: Highlight the found range? else message("'" .. query .. "' not found") end end -- Example binding: Find "TODO" bind_key("C-o", function() find_next("TODO") end) -- Line swapping functions (like Emacs M-up/down or VS Code Alt+arrows) function swap_line_up() local buf = editor.buffer local cursor = editor.cursor -- Can't move first line up if cursor.line == 0 then message("Already at first line") return end message("DEBUG: Starting swap_line_up, cursor at line " .. cursor.line) -- Get the current line and above line text local current_line = buf:line(cursor.line) local above_line = buf:line(cursor.line - 1) -- Strategy: Replace both lines with them swapped -- Delete from start of line above to end of current line (not including next line) local delete_start = lumacs.Position(cursor.line - 1, 0) local delete_end = lumacs.Position(cursor.line, string.len(current_line)) local range = lumacs.Range(delete_start, delete_end) buf:erase(range) -- Insert them back in swapped order -- Add extra newline if above_line is empty to preserve it local insert_pos = lumacs.Position(cursor.line - 1, 0) local text = current_line .. "\n" .. above_line if above_line == "" then text = text .. "\n" end buf:insert(insert_pos, text) -- Move cursor up to follow the line editor.cursor = lumacs.Position(cursor.line - 1, cursor.column) message("Swapped line up") end function swap_line_down() local buf = editor.buffer local cursor = editor.cursor -- Can't move last line down if cursor.line >= buf:line_count() - 1 then message("Already at last line") return end -- Get the current line and the line below local current_line = buf:line(cursor.line) local below_line = buf:line(cursor.line + 1) -- Strategy: Replace both lines with them swapped -- Delete from start of current line to end of line below (not including line after) local delete_start = lumacs.Position(cursor.line, 0) local delete_end = lumacs.Position(cursor.line + 1, string.len(below_line)) local range = lumacs.Range(delete_start, delete_end) buf:erase(range) -- Insert them back in swapped order -- Add extra newline if current_line is empty to preserve it local insert_pos = lumacs.Position(cursor.line, 0) local text = below_line .. "\n" .. current_line if current_line == "" then text = text .. "\n" end buf:insert(insert_pos, text) -- Move cursor down to follow the line editor.cursor = lumacs.Position(cursor.line + 1, cursor.column) message("Swapped line down") end -- Bind to M-ArrowUp and M-ArrowDown (Meta/Alt + arrows) bind_key("M-ArrowUp", swap_line_up) bind_key("M-ArrowDown", swap_line_down) -- ============================================================================ -- WINDOW MANAGEMENT -- ============================================================================ -- Split horizontal (like Emacs C-x 2, simplified to M-2) bind_key("M-2", function() editor:split_horizontally() message("Split horizontally") end) -- Split vertical (like Emacs C-x 3, simplified to M-3) bind_key("M-3", function() editor:split_vertically() message("Split vertically") end) -- Close window (like Emacs C-x 0, simplified to M-0) bind_key("M-0", function() editor:close_window() message("Closed window") end) -- Command Mode (Minibuffer) bind_key("M-x", function() editor:command_mode() end) -- ============================================================================ -- MARK AND REGION (Emacs-style selection) -- ============================================================================ -- C-@ or C-SPC (set-mark-command) - Set the mark at cursor bind_key("C-@", function() local buf = editor.buffer local cursor = editor.cursor buf:set_mark(cursor) message("Mark set") end) -- For terminals that don't support C-@, also bind to C-SPC (but C-SPC is hard to detect) -- Most terminals send C-@ for C-SPC, so the above should work -- C-x C-x (exchange-point-and-mark) - Swap cursor and mark bind_key("C-x C-x", function() local buf = editor.buffer local mark = buf.mark if not mark then message("No mark set") return end local cursor = editor.cursor buf:set_mark(cursor) -- Set mark at old cursor position editor.cursor = mark -- Move cursor to old mark position message("Mark and point exchanged") end) -- C-x h (mark-whole-buffer) - Select entire buffer bind_key("C-x h", function() local buf = editor.buffer -- Set mark at beginning buf:set_mark(lumacs.Position(0, 0)) -- Move cursor to end local last_line = buf:line_count() - 1 local last_col = #buf:line(last_line) editor.cursor = lumacs.Position(last_line, last_col) message("Buffer marked") end) -- ============================================================================ -- KILL RING (Emacs cut/copy/paste) -- ============================================================================ -- C-w (kill-region) - Cut selection bind_key("C-w", function() editor:kill_region() end) -- M-w (kill-ring-save) - Copy selection bind_key("M-w", function() editor:copy_region_as_kill() end) -- C-k (kill-line) - Cut from cursor to end of line bind_key("C-k", function() editor:kill_line() message("Killed line") end) -- C-y (yank) - Paste bind_key("C-y", function() editor:yank() end) -- M-y (yank-pop) - Cycle through kill ring after yanking bind_key("M-y", function() editor:yank_pop() end) -- M-d (kill-word) - Kill word forward bind_key("M-d", function() editor:kill_word() end) -- M-Backspace (backward-kill-word) - Kill word backward bind_key("M-Backspace", function() editor:backward_kill_word() end) -- ============================================================================ -- UNDO/REDO -- ============================================================================ -- C-/ or C-_ for undo (traditional Emacs binding) bind_key("C-/", function() if editor:undo() then message("Undid change") else message("Nothing to undo") end end) -- Also keep C-z for undo (common in other editors) bind_key("C-z", function() if editor:undo() then message("Undid change") else message("Nothing to undo") end end) -- C-x u for redo (less common but sometimes used) bind_key("C-x u", function() if editor:redo() then message("Redid change") else message("Nothing to redo") end end) -- Manual re-highlight key (re-applies current major mode's highlighting) bind_key("C-l", function() local mode_name = current_major_mode() local mode = major_modes[mode_name] if mode and mode.highlight then mode.highlight() -- Debug: Count applied styles local buf = editor.buffer local styles_count = 0 for line = 0, buf:line_count() - 1 do local styles = buf:get_line_styles(line) styles_count = styles_count + #styles end message(string.format("Re-highlighted with %s (%d styles)", mode_name, styles_count)) else message("Current mode has no highlighting: " .. mode_name) end end) -- Mode information and control bind_key("C-h m", function() local mode_name = current_major_mode() local buf = editor.buffer local buf_name = buf:name() -- Get active minor modes local minor_list = {} if buffer_minor_modes[buf_name] then for mode, _ in pairs(buffer_minor_modes[buf_name]) do table.insert(minor_list, mode) end end local minor_str = #minor_list > 0 and table.concat(minor_list, ", ") or "none" message(string.format("Major: %s | Minor: %s", mode_name, minor_str)) end) -- Test Escape key binding bind_key("Escape", function() message("Escape pressed! (Direct binding works)") end) -- C-x sequence bindings (Emacs style) bind_key("C-x o", function() editor:next_window() message("Switched window with C-x o") end) bind_key("C-x 2", function() editor:split_horizontally() message("Split horizontally with C-x 2") end) bind_key("C-x 3", function() editor:split_vertically() message("Split vertically with C-x 3") end) bind_key("C-x 0", function() editor:close_window() message("Closed window with C-x 0") end) -- ============================================================================ -- BUFFER MANAGEMENT (C-x b, C-x C-b, C-x k) -- ============================================================================ -- C-x b: Switch to buffer (with tab completion) bind_key("C-x b", function() editor:buffer_switch_mode() end) -- C-x k: Kill buffer (with tab completion) bind_key("C-x k", function() editor:kill_buffer_mode() end) -- C-x C-b: List all buffers bind_key("C-x C-b", function() local buffer_info = editor:get_all_buffer_info() if #buffer_info == 0 then message("No buffers open") return end -- Format buffer list local lines = {} table.insert(lines, "Buffer List:") table.insert(lines, "------------") table.insert(lines, "") table.insert(lines, string.format("%-3s %-20s %-10s %s", "Mod", "Name", "Size", "File")) table.insert(lines, string.format("%-3s %-20s %-10s %s", "---", "----", "----", "----")) for i, info in ipairs(buffer_info) do local modified = info.modified and " * " or " " local filepath = "" if info.filepath then filepath = tostring(info.filepath) end local line = string.format("%s %-20s %-10d %s", modified, info.name, info.size, filepath ) table.insert(lines, line) end local list_text = table.concat(lines, "\n") local list_buf_name = "*Buffer List*" -- Switch to or create buffer local list_buf = editor:get_buffer_by_name(list_buf_name) if list_buf then editor:switch_buffer_in_window(list_buf_name) else editor:new_buffer(list_buf_name) end local buf = editor.buffer buf:clear() buf:insert(lumacs.Position(0,0), list_text) editor:goto_beginning() message(string.format("Buffer list (%d buffers)", #buffer_info)) end) -- ============================================================================ -- CASE CONVERSION (M-u, M-l, M-c) -- ============================================================================ -- M-u (upcase-word) bind_key("M-u", function() local start_pos = editor.cursor editor:move_forward_word() local end_pos = editor.cursor local range = lumacs.Range(start_pos, end_pos) local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.upper(text)) end end) -- M-l (downcase-word) bind_key("M-l", function() local start_pos = editor.cursor editor:move_forward_word() local end_pos = editor.cursor local range = lumacs.Range(start_pos, end_pos) local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.lower(text)) end end) -- M-c (capitalize-word) bind_key("M-c", function() local start_pos = editor.cursor editor:move_forward_word() local end_pos = editor.cursor local range = lumacs.Range(start_pos, end_pos) local text = editor.buffer:get_text_in_range(range) if #text > 0 then local cap_text = text:sub(1,1):upper() .. text:sub(2):lower() editor.buffer:replace(range, cap_text) end end) -- C-x C-u (upcase-region) bind_key("C-x C-u", function() local range = editor.buffer:get_region(editor.cursor) if not range then message("No active region") return end local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.upper(text)) editor.buffer:deactivate_mark() end end) -- C-x C-l (downcase-region) bind_key("C-x C-l", function() local range = editor.buffer:get_region(editor.cursor) if not range then message("No active region") return end local text = editor.buffer:get_text_in_range(range) if #text > 0 then editor.buffer:replace(range, string.lower(text)) editor.buffer:deactivate_mark() end end) -- ============================================================================ -- COMMENTING (M-;) -- ============================================================================ function escape_pattern(text) return text:gsub("([^%w])", "%%%1") end function comment_dwim() local mode_name = current_major_mode() local mode = major_modes[mode_name] local prefix = mode and mode.comment_syntax or "--" local escaped_prefix = escape_pattern(prefix) local range = editor.buffer:get_region(editor.cursor) if range then -- Region handling local start_line = range.start.line local end_line = range["end"].line -- Check if all lines are commented local all_commented = true for i = start_line, end_line do local line = editor.buffer:line(i) -- Ignore empty lines for decision if #line > 0 and not string.match(line, "^%s*" .. escaped_prefix) then all_commented = false break end end -- Apply change for i = start_line, end_line do local line = editor.buffer:line(i) local line_range = lumacs.Range(lumacs.Position(i, 0), lumacs.Position(i, #line)) if #line > 0 then if all_commented then -- Uncomment local s, e = string.find(line, escaped_prefix) if s then local suffix = line:sub(e+1) if suffix:sub(1,1) == " " then suffix = suffix:sub(2) end local new_line = line:sub(1, s-1) .. suffix editor.buffer:replace(line_range, new_line) end else -- Comment (if not already commented) if not string.match(line, "^%s*" .. escaped_prefix) then local indent = string.match(line, "^%s*") local content = line:sub(#indent + 1) local new_line = indent .. prefix .. " " .. content editor.buffer:replace(line_range, new_line) end end end end else -- Single line local line_num = editor.cursor.line local line = editor.buffer:line(line_num) local line_range = lumacs.Range(lumacs.Position(line_num, 0), lumacs.Position(line_num, #line)) if string.match(line, "^%s*" .. escaped_prefix) then -- Uncomment local s, e = string.find(line, escaped_prefix) if s then local suffix = line:sub(e+1) if suffix:sub(1,1) == " " then suffix = suffix:sub(2) end local new_line = line:sub(1, s-1) .. suffix editor.buffer:replace(line_range, new_line) end else -- Comment local indent = string.match(line, "^%s*") local content = line:sub(#indent + 1) local new_line = indent .. prefix .. " " .. content editor.buffer:replace(line_range, new_line) end end end bind_key("M-;", comment_dwim) -- ============================================================================ -- REGISTERS (C-x r s, C-x r i) -- ============================================================================ -- Helper function to prompt for register character function get_register_char(prompt_msg) message(prompt_msg) -- For now we'll use a simple approach - this would need UI integration -- In a full implementation, this would wait for a single character input -- For demo purposes, we'll use 'a' as default return 'a' end -- C-x r s (copy-to-register) - Save region to register bind_key("C-x r s", function() local register_char = get_register_char("Save to register:") editor:copy_region_to_register(register_char) end) -- C-x r i (insert-register) - Insert from register bind_key("C-x r i", function() local register_char = get_register_char("Insert register:") editor:yank_from_register(register_char) end) -- ============================================================================ -- RECTANGLES (C-x r k/y/t) -- ============================================================================ -- C-x r k (kill-rectangle) - Cut rectangular region bind_key("C-x r k", function() editor:kill_rectangle() end) -- C-x r y (yank-rectangle) - Paste rectangular region bind_key("C-x r y", function() editor:yank_rectangle() end) -- C-x r t (string-rectangle) - Fill rectangle with text bind_key("C-x r t", function() -- For demo purposes, we'll fill with asterisks -- In a full implementation, this would prompt for text input editor:string_rectangle("*") message("Rectangle filled with '*' - C-x r t demo") end) -- ============================================================================ -- KEYBOARD MACROS (F3, F4) -- ============================================================================ -- F3 - start-kbd-macro bind_key("F3", function() editor:start_kbd_macro() end) -- F4 - end-kbd-macro or call-last-kbd-macro bind_key("F4", function() editor:end_kbd_macro_or_call() end) -- ============================================================================ -- INCREMENTAL SEARCH (C-s, C-r) -- ============================================================================ -- C-s (save-buffer) - changed from isearch to save for now bind_key("C-s", function() local buf = editor.buffer if buf:save() then message("Buffer saved: " .. buf:name()) else message("Failed to save buffer") end end) -- C-r (isearch-backward) bind_key("C-r", function() editor:isearch_backward_mode() end) -- Note: C-k and C-s already defined above, removed duplicates -- ============================================================================ -- CONFIGURATION FUNCTIONS -- ============================================================================ -- Toggle line numbers on/off function toggle_line_numbers() local config = editor.config local current = config:get_bool("show_line_numbers", true) config:set("show_line_numbers", not current) if not current then message("Line numbers enabled") else message("Line numbers disabled") end end -- Set line number width function set_line_number_width(width) editor.config:set("line_number_width", width) message("Line number width set to " .. width) end -- Show current configuration function show_config() local config = editor.config local show_nums = config:get_bool("show_line_numbers", true) local width = config:get_int("line_number_width", 6) message(string.format("Line numbers: %s, Width: %d", show_nums and "on" or "off", width)) end -- Toggle modeline (status bar for each window) function function toggle_modeline() local current = editor.config:get_bool("show_modeline", true) editor.config:set_bool("show_modeline", not current) if current then message("Modeline disabled") else message("Modeline enabled") end end -- Bind configuration functions bind_key("C-x l", toggle_line_numbers) -- C-x l to toggle line numbers bind_key("C-x m", toggle_modeline) -- C-x m to toggle modeline bind_key("C-x C-c", function() editor:quit() end) -- C-x C-c to quit bind_key("C-x C-s", show_config) -- C-x C-s to show config -- Load theme configuration dofile("themes.lua") -- Welcome message message("Lumacs ready! C-k=kill, C-y=yank, C-@=mark, C-w=cut, M-w=copy, M-f/b=word, C-v/M-v=page") -- Auto-activate mode for initial buffer auto_activate_major_mode() -- ============================================================================ -- COMMAND REGISTRY (M-x support) -- ============================================================================ lumacs.command_registry = {} function define_command(name, func, doc) lumacs.command_registry[name] = { func = func, doc = doc or "No documentation" } end function execute_extended_command(name) local cmd = lumacs.command_registry[name] if cmd then cmd.func() return true end return false end function get_command_names() local names = {} for name, _ in pairs(lumacs.command_registry) do table.insert(names, name) end table.sort(names) return names end -- File and Buffer Commands define_command("save-buffer", function() if editor.buffer:save() then message("Saved " .. editor.buffer:name()) else message("Save failed") end end, "Save current buffer") define_command("find-file", function() editor:find_file_mode() end, "Find file") define_command("kill-buffer", function() editor:kill_buffer_mode() end, "Kill buffer") define_command("switch-buffer", function() editor:buffer_switch_mode() end, "Switch buffer") -- Alias for switch-buffer to match Emacs expectations define_command("switch-to-buffer", function() editor:buffer_switch_mode() end, "Switch buffer (Alias)") define_command("list-buffers", function() -- Use existing implementation via keybinding or duplicate logic? -- Calling the function bound to C-x C-b if we can find it, or just define it here. -- Since C-x C-b binding is anonymous function, I'll copy logic or better, -- extract list-buffers logic in init.lua (future refactor). -- For now, I'll just invoke the binding C-x C-b if possible? No. -- I'll just leave it for now or copy-paste the logic. -- Copying logic is safer for now. local buffer_info = editor:get_all_buffer_info() if #buffer_info == 0 then message("No buffers open") return end local lines = {} table.insert(lines, "Buffer List:") table.insert(lines, "------------") table.insert(lines, "") table.insert(lines, string.format("%-3s %-20s %-10s %s", "Mod", "Name", "Size", "File")) table.insert(lines, string.format("%-3s %-20s %-10s %s", "---", "----", "----", "----")) for i, info in ipairs(buffer_info) do local modified = info.modified and " * " or " " local filepath = "" if info.filepath then filepath = tostring(info.filepath) end table.insert(lines, string.format("%s %-20s %-10d %s", modified, info.name, info.size, filepath)) end local list_text = table.concat(lines, "\n") local list_buf_name = "*Buffer List*" local list_buf = editor:get_buffer_by_name(list_buf_name) if list_buf then editor:switch_buffer_in_window(list_buf_name) else editor:new_buffer(list_buf_name) end editor.buffer:clear() editor.buffer:insert(lumacs.Position(0,0), list_text) editor:goto_beginning() end, "List all buffers") -- Navigation define_command("next-line", function() editor:move_down() end, "Move cursor down") define_command("previous-line", function() editor:move_up() end, "Move cursor up") define_command("forward-char", function() editor:move_right() end, "Move cursor right") define_command("backward-char", function() editor:move_left() end, "Move cursor left") define_command("forward-word", function() editor:move_forward_word() end, "Move forward one word") define_command("backward-word", function() editor:move_backward_word() end, "Move backward one word") define_command("beginning-of-buffer", function() editor:goto_beginning() end, "Go to beginning of buffer") define_command("end-of-buffer", function() editor:goto_end() end, "Go to end of buffer") define_command("scroll-up-command", function() editor:page_down() end, "Page down") define_command("scroll-down-command", function() editor:page_up() end, "Page up") -- Window Management define_command("split-window-below", function() editor:split_horizontally() end, "Split window horizontally") define_command("split-window-right", function() editor:split_vertically() end, "Split window vertically") define_command("delete-window", function() editor:close_window() end, "Close current window") define_command("other-window", function() editor:next_window() end, "Select other window") define_command("delete-other-windows", function() -- Simplified: keep closing others until only 1? -- Or just implement properly in C++ later. -- For now, assume users use C-x 1 if implemented? -- C-x 1 isn't implemented in init.lua yet. message("delete-other-windows not implemented yet") end, "Delete all other windows") -- Editing define_command("kill-line", function() editor:kill_line() end, "Kill rest of line") define_command("kill-region", function() editor:kill_region() end, "Kill selected region") define_command("copy-region-as-kill", function() editor:copy_region_as_kill() end, "Copy region") define_command("yank", function() editor:yank() end, "Paste from kill ring") define_command("undo", function() if editor:undo() then message("Undid") else message("No undo") end end, "Undo last change") define_command("redo", function() if editor:redo() then message("Redid") else message("No redo") end end, "Redo last undo") -- Modes define_command("lua-mode", function() activate_major_mode("lua-mode") end, "Switch to Lua mode") define_command("fundamental-mode", function() activate_major_mode("fundamental-mode") end, "Switch to Fundamental mode") define_command("auto-save-mode", function() toggle_minor_mode("auto-save-mode") end, "Toggle auto-save") message("Commands loaded. Try M-x list-buffers")