local theme = require("core.theme") local themes = require("core.themes") local state = require("core.state") local ui = require("core.ui") local command = require("core.commands") local cmdline = require("core.cmdline") local hooks = require("core.hooks") local motion = require("core.motion") local modes = require("core.modes") local M = {} state.hooks = hooks.new_store() local function current_palette() return themes.all[state.current_theme] end local function set_theme(name) local palette = themes.all[name] if not palette then ui.set_message(state, "Unknown theme: " .. tostring(name)) return false end local prev = state.current_theme state.current_theme = name theme.apply_base16(palette) ui.setup_layout(state, palette) ui.set_message(state, "Theme: " .. name) ui.update_modeline(state) hooks.run(state.hooks, "theme_changed", name, prev) return true end local function update_cursor_for_current_mode() if state.command_mode then editor.set_cursor_shape("block") editor.set_cursor_thickness(2) return end if modes.is_insert(state) then editor.set_cursor_shape("bar") editor.set_cursor_thickness(2) return end editor.set_cursor_shape("block") editor.set_cursor_thickness(2) end local function on_commandline_key(key) if key == "escape" then cmdline.exit(state, ui, current_palette()) ui.set_message(state, "lua cmdline cancelled") update_cursor_for_current_mode() hooks.run(state.hooks, "commandline_exit", "cancel") return true end if key == "backspace" then state.command_text = state.command_text:sub(1, #state.command_text - 1) ui.update_commandline_buffer(state) hooks.run(state.hooks, "commandline_change", state.command_text) return true end if key == "enter" then local line = state.command_text local kind = state.command_kind cmdline.exit(state, ui, current_palette()) update_cursor_for_current_mode() if line ~= "" then hooks.run(state.hooks, "commandline_execute", line) local ok, message if kind == "command" then ok, message = cmdline.eval_command(state, line) else ok, message = cmdline.eval_line(line) end if message then ui.set_message(state, message) end hooks.run(state.hooks, "commandline_result", line, ok, message) return ok end ui.set_message(state, "lua cmdline empty") hooks.run(state.hooks, "commandline_exit", "empty") return true end return true end local function define_editor_commands() command.define(state, "quit", function() hooks.run(state.hooks, "before_quit") editor.request_quit() end) command.define(state, "save", function() local path = ui.current_buffer_label() local ok = editor.save_file(nil) if ok then ui.set_message(state, "Saved " .. path) else ui.set_message(state, "Save failed (no file path yet)") end ui.update_modeline(state) hooks.run(state.hooks, "after_save", ok, path) end) command.define(state, "new-buffer", function() local index = editor.new_buffer() if not index then ui.set_message(state, "Failed to create new buffer") return end ui.set_message(state, string.format("New buffer #%d", index)) ui.update_modeline(state) end) command.define(state, "next-buffer", function() local count = editor.buffer_count() if count <= 0 then return end local current = editor.current_buffer_index() local next_index = (current + 1) % count editor.switch_buffer(next_index) local path = editor.buffer_path() if path then ui.set_message(state, "Buffer: " .. path) else ui.set_message(state, string.format("Buffer #%d", next_index)) end ui.update_modeline(state) end) command.define(state, "prev-buffer", function() local count = editor.buffer_count() if count <= 0 then return end local current = editor.current_buffer_index() local prev_index = (current - 1 + count) % count editor.switch_buffer(prev_index) local path = editor.buffer_path() if path then ui.set_message(state, "Buffer: " .. path) else ui.set_message(state, string.format("Buffer #%d", prev_index)) end ui.update_modeline(state) end) command.define(state, "list-buffers", function() local count = editor.buffer_count() local current = editor.current_buffer_index() local names = {} for i = 0, count - 1 do local path = editor.buffer_path_at(i) local label = path or string.format("*buffer-%d*", i) local dirty = editor.buffer_modified_at(i) and "*" or "" local mark = (i == current) and ">" or " " names[#names + 1] = string.format("%s%d:%s%s", mark, i, label, dirty) end ui.set_message(state, table.concat(names, " | ")) end) command.define(state, "buffer", function() ui.set_message(state, "Usage: :buffer ") end) command.define(state, "buffer-switch", function(index) if type(index) ~= "number" then ui.set_message(state, "buffer-switch expects number") return end if editor.switch_buffer(index) then local path = editor.buffer_path() if path then ui.set_message(state, "Buffer: " .. path) else ui.set_message(state, string.format("Buffer #%d", index)) end ui.update_modeline(state) else ui.set_message(state, string.format("Invalid buffer index: %s", tostring(index))) end end) command.define(state, "write", function() command.call(state, "save") end) command.define(state, "w", function() command.call(state, "write") end) command.define(state, "write-quit", function() local ok = editor.save_file(nil) local path = ui.current_buffer_label() if ok then ui.set_message(state, "Saved " .. path) ui.update_modeline(state) hooks.run(state.hooks, "after_save", ok, path) hooks.run(state.hooks, "before_quit") editor.request_quit() else ui.set_message(state, "Save failed (no file path yet)") ui.update_modeline(state) hooks.run(state.hooks, "after_save", ok, path) end end) command.define(state, "wq", function() command.call(state, "write-quit") end) command.define(state, "help", function() ui.set_message(state, "Modes: i/a/o insert, esc normal. Normal: hjkl w b d ^ $ : | l lua") end) command.define(state, "toggle-theme", function() if state.current_theme == "base16-default-light" then set_theme("base16-gruvbox-dark-hard") else set_theme("base16-default-light") end end) command.define(state, "lua-commandline", function() if state.command_mode then cmdline.exit(state, ui, current_palette()) hooks.run(state.hooks, "commandline_exit", "toggle") else cmdline.enter(state, ui, current_palette(), "lua") hooks.run(state.hooks, "commandline_enter") end update_cursor_for_current_mode() end) command.define(state, "named-command-prompt", function() if state.command_mode then cmdline.exit(state, ui, current_palette()) hooks.run(state.hooks, "commandline_exit", "toggle") else cmdline.enter(state, ui, current_palette(), "command") hooks.run(state.hooks, "commandline_enter") end update_cursor_for_current_mode() end) command.define(state, "enter-insert-mode", function() modes.set_mode(state, "insert") ui.update_modeline(state) update_cursor_for_current_mode() end) command.define(state, "append-mode", function() editor.move_right() modes.set_mode(state, "insert") ui.update_modeline(state) update_cursor_for_current_mode() end) command.define(state, "open-below", function() editor.move_end_of_line() editor.insert("\n") modes.set_mode(state, "insert") ui.update_modeline(state) update_cursor_for_current_mode() end) command.define(state, "enter-normal-mode", function() modes.set_mode(state, "normal") state.leader_pending = false ui.update_modeline(state) update_cursor_for_current_mode() end) command.define(state, "move-left", function() editor.move_left() end) command.define(state, "move-right", function() editor.move_right() end) command.define(state, "move-up", function() editor.move_up() end) command.define(state, "move-down", function() editor.move_down() end) command.define(state, "delete-char", function() editor.delete_forward() end) command.define(state, "beginning-of-line", function() editor.move_beginning_of_line() end) command.define(state, "end-of-line", function() editor.move_end_of_line() end) command.define(state, "forward-word", function() motion.forward_word() end) command.define(state, "backward-word", function() motion.backward_word() end) command.define(state, "backspace", function() editor.backspace() end) command.define(state, "delete-forward", function() editor.delete_forward() end) command.define(state, "newline", function() editor.insert("\n") end) command.define(state, "tab", function() editor.insert(" ") end) command.bind(state, "C-q", "quit") command.bind(state, "C-s", "save") command.bind(state, "C-h", "help") command.bind(state, "C-t", "toggle-theme") command.bind(state, "C-a", "beginning-of-line") command.bind(state, "C-e", "end-of-line") command.bind(state, "C-f", "forward-word") command.bind(state, "C-b", "backward-word") state.leader_map["l"] = "lua-commandline" command.bind(state, "left", "move-left") command.bind(state, "right", "move-right") command.bind(state, "up", "move-up") command.bind(state, "down", "move-down") command.bind(state, "backspace", "backspace") command.bind(state, "delete", "delete-forward") command.bind(state, "enter", "newline") command.bind(state, "tab", "tab") end function M.add_hook(event_name, fn) return hooks.add(state.hooks, event_name, fn) end function M.bind_key(key, target) if type(target) == "string" then command.bind(state, key, target) return true end if type(target) == "function" then return command.bind_fn(state, key, target) end return false, "bind target must be command name or function" end function M.bind_leader(key, target) if type(key) ~= "string" or key == "" then return false, "leader key must be non-empty" end if type(target) == "string" then state.leader_map[key] = target return true end if type(target) == "function" then local name = string.format("leader:%s", key) command.define(state, name, target) state.leader_map[key] = name return true end return false, "leader bind target must be command name or function" end local function try_extended_named_command(input) local name, arg = input:match("^(%S+)%s+(.+)$") if not name then return false end if name ~= "buffer" and name ~= "b" then return false end local index = tonumber(arg) if not index then return false end command.call(state, "buffer-switch", index) return true end function M.bootstrap() state.mode_line_buffer = editor.aux_buffer_new() state.message_buffer = editor.aux_buffer_new() state.command_buffer = editor.aux_buffer_new() if not state.mode_line_buffer or not state.message_buffer or not state.command_buffer then error("failed to allocate UI buffers") end editor.set_font("monospace:size=14") set_theme(state.current_theme) update_cursor_for_current_mode() ui.set_message(state, state.message_text) ui.update_commandline_buffer(state) define_editor_commands() ui.update_modeline(state) ui.set_message(state, "ready") hooks.run(state.hooks, "bootstrap") end function M.on_open_file(path) if not path or path == "" then return false end local ok = editor.open_file(path) if ok then ui.set_message(state, "Opened " .. path) else ui.set_message(state, "Could not open: " .. path) end ui.update_modeline(state) hooks.run(state.hooks, "after_open_file", ok, path) return ok end function M.on_key(key) hooks.run(state.hooks, "before_key", key) if hooks.run_first_truthy(state.hooks, "intercept_key", key) then hooks.run(state.hooks, "after_key", key, true) return true end if state.command_mode then local handled = on_commandline_key(key) hooks.run(state.hooks, "after_key", key, handled) return handled end if modes.is_insert(state) and key == "escape" then state.suppress_next_text = true command.call(state, "enter-normal-mode") hooks.run(state.hooks, "after_key", key, true) return true end if modes.is_normal(state) then if state.leader_pending then state.suppress_next_text = true state.leader_pending = false local command_name = state.leader_map[key] if command_name then command.call(state, command_name) else ui.set_message(state, string.format("No mapping for '%s'", key)) end ui.update_modeline(state) hooks.run(state.hooks, "after_key", key, true) return true end if key == ":" then state.suppress_next_text = true command.call(state, "named-command-prompt") hooks.run(state.hooks, "after_key", key, true) return true end if key == state.leader_key then state.suppress_next_text = true state.leader_pending = true ui.update_modeline(state) hooks.run(state.hooks, "after_key", key, true) return true end if key == "i" then state.suppress_next_text = true command.call(state, "enter-insert-mode") hooks.run(state.hooks, "after_key", key, true) return true end if key == "a" then state.suppress_next_text = true command.call(state, "append-mode") hooks.run(state.hooks, "after_key", key, true) return true end if key == "o" then state.suppress_next_text = true command.call(state, "open-below") hooks.run(state.hooks, "after_key", key, true) return true end if key == "h" then state.suppress_next_text = true command.call(state, "move-left") hooks.run(state.hooks, "after_key", key, true) return true end if key == "j" then state.suppress_next_text = true command.call(state, "move-down") hooks.run(state.hooks, "after_key", key, true) return true end if key == "k" then state.suppress_next_text = true command.call(state, "move-up") hooks.run(state.hooks, "after_key", key, true) return true end if key == "l" then state.suppress_next_text = true command.call(state, "move-right") hooks.run(state.hooks, "after_key", key, true) return true end if key == "w" then state.suppress_next_text = true command.call(state, "forward-word") hooks.run(state.hooks, "after_key", key, true) return true end if key == "b" then state.suppress_next_text = true command.call(state, "backward-word") hooks.run(state.hooks, "after_key", key, true) return true end if key == "d" then state.suppress_next_text = true command.call(state, "delete-char") hooks.run(state.hooks, "after_key", key, true) return true end if key == "^" then state.suppress_next_text = true command.call(state, "beginning-of-line") hooks.run(state.hooks, "after_key", key, true) return true end if key == "$" then state.suppress_next_text = true command.call(state, "end-of-line") hooks.run(state.hooks, "after_key", key, true) return true end end local command_name = state.keymap[key] if not command_name then hooks.run(state.hooks, "after_key", key, false) return false end local ok, err = command.call(state, command_name) if not ok and err then ui.set_message(state, err) end hooks.run(state.hooks, "after_key", key, ok) return ok end function M.try_named_command_extension(input) return try_extended_named_command(input) end function M.on_text(text) hooks.run(state.hooks, "before_text", text) if state.suppress_next_text then state.suppress_next_text = false hooks.run(state.hooks, "after_text", text, true) return true end if hooks.run_first_truthy(state.hooks, "intercept_text", text) then hooks.run(state.hooks, "after_text", text, true) return true end if state.command_mode then state.command_text = state.command_text .. text ui.update_commandline_buffer(state) hooks.run(state.hooks, "after_text", text, true) return true end if not modes.is_insert(state) then hooks.run(state.hooks, "after_text", text, true) return true end local ok = editor.insert(text) ui.update_modeline(state) hooks.run(state.hooks, "after_text", text, ok) return ok end function M.on_tick() hooks.run(state.hooks, "before_tick") ui.setup_layout(state, current_palette()) ui.update_modeline(state) hooks.run(state.hooks, "after_layout") hooks.run(state.hooks, "after_tick") end return M