-- 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) -- Custom command: Save buffer 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) -- 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() 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) -- ============================================================================ -- 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) -- Test control keys (ncurses versions) bind_key("C-k", function() message("C-k pressed! (Control key working with ncurses)") end) bind_key("C-s", function() local buf = editor.buffer if buf:save() then message("Buffer saved with C-s (ncurses)!") else message("Failed to save buffer") end end) -- 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()