diff options
Diffstat (limited to 'runtime/lua/core/app.lua')
| -rw-r--r-- | runtime/lua/core/app.lua | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/runtime/lua/core/app.lua b/runtime/lua/core/app.lua new file mode 100644 index 0000000..a596f59 --- /dev/null +++ b/runtime/lua/core/app.lua @@ -0,0 +1,595 @@ +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 <index>") + 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 ^ $ : | <leader>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 <leader> 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 |
