diff options
| author | Collin Williams <96917990+bluedragon1221@users.noreply.github.com> | 2026-05-11 15:07:55 -0500 |
|---|---|---|
| committer | Collin Williams <96917990+bluedragon1221@users.noreply.github.com> | 2026-05-11 15:07:55 -0500 |
| commit | cebb280fe546190e55e071b6adbf37cd47950f34 (patch) | |
| tree | 6e7a7605f8832b1c1bd7e467884a8792a5e1fab4 /runtime/lua | |
initial commit
Diffstat (limited to 'runtime/lua')
| -rw-r--r-- | runtime/lua/core/app.lua | 595 | ||||
| -rw-r--r-- | runtime/lua/core/cmdline.lua | 63 | ||||
| -rw-r--r-- | runtime/lua/core/commands.lua | 41 | ||||
| -rw-r--r-- | runtime/lua/core/hooks.lua | 61 | ||||
| -rw-r--r-- | runtime/lua/core/modes.lua | 20 | ||||
| -rw-r--r-- | runtime/lua/core/motion.lua | 66 | ||||
| -rw-r--r-- | runtime/lua/core/state.lua | 35 | ||||
| -rw-r--r-- | runtime/lua/core/theme.lua | 34 | ||||
| -rw-r--r-- | runtime/lua/core/themes.lua | 8 | ||||
| -rw-r--r-- | runtime/lua/core/ui.lua | 84 | ||||
| -rw-r--r-- | runtime/lua/themes/base16-default-light.lua | 19 | ||||
| -rw-r--r-- | runtime/lua/themes/base16-gruvbox-dark-hard.lua | 19 |
12 files changed, 1045 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 diff --git a/runtime/lua/core/cmdline.lua b/runtime/lua/core/cmdline.lua new file mode 100644 index 0000000..4d7e44a --- /dev/null +++ b/runtime/lua/core/cmdline.lua @@ -0,0 +1,63 @@ +local M = {} + +function M.eval_line(line) + local chunk, load_err = load("return " .. line, "=lemacs-cmd", "t", _G) + if not chunk then + chunk, load_err = load(line, "=lemacs-cmd", "t", _G) + end + + if not chunk then + return false, "lua compile error: " .. tostring(load_err) + end + + local ok, result = pcall(chunk) + if not ok then + return false, "lua runtime error: " .. tostring(result) + end + + if result ~= nil then + return true, "=> " .. tostring(result) + end + + return true, "lua ok" +end + +function M.eval_command(state, line) + local name = (line or ""):gsub("^%s+", ""):gsub("%s+$", "") + if name == "" then + return false, "command prompt empty" + end + + local app = require("core.app") + if app.try_named_command_extension(name) then + return true, nil + end + + local command = require("core.commands") + local ok, err = command.call(state, name) + if not ok then + return false, err + end + + return true, nil +end + +function M.enter(state, ui, palette, kind) + state.command_mode = true + state.command_kind = kind or "lua" + state.command_text = "" + ui.update_commandline_buffer(state) + ui.update_modeline(state) + ui.setup_layout(state, palette) +end + +function M.exit(state, ui, palette) + state.command_mode = false + state.command_kind = "lua" + state.command_text = "" + ui.update_commandline_buffer(state) + ui.update_modeline(state) + ui.setup_layout(state, palette) +end + +return M diff --git a/runtime/lua/core/commands.lua b/runtime/lua/core/commands.lua new file mode 100644 index 0000000..2aa484d --- /dev/null +++ b/runtime/lua/core/commands.lua @@ -0,0 +1,41 @@ +local M = {} + +function M.define(state, name, fn) + state.commands[name] = fn +end + +function M.bind(state, key, command_name) + state.keymap[key] = command_name +end + +function M.call(state, name, ...) + local fn = state.commands[name] + if not fn then + return false, "Unknown command: " .. tostring(name) + end + + local ok, err = pcall(fn, ...) + if not ok then + return false, tostring(err) + end + + return true +end + +function M.bind_fn(state, key, fn) + if type(key) ~= "string" or key == "" then + return false, "key must be a non-empty string" + end + if type(fn) ~= "function" then + return false, "bind_fn requires a function" + end + + local name = string.format("anon:%d", state.next_anon_command_id) + state.next_anon_command_id = state.next_anon_command_id + 1 + + state.commands[name] = fn + state.keymap[key] = name + return true +end + +return M diff --git a/runtime/lua/core/hooks.lua b/runtime/lua/core/hooks.lua new file mode 100644 index 0000000..9623e9c --- /dev/null +++ b/runtime/lua/core/hooks.lua @@ -0,0 +1,61 @@ +local M = {} + +function M.new_store() + return {} +end + +function M.add(store, event_name, fn) + if type(event_name) ~= "string" or event_name == "" then + return false, "event name must be a non-empty string" + end + if type(fn) ~= "function" then + return false, "hook must be a function" + end + + local bucket = store[event_name] + if not bucket then + bucket = {} + store[event_name] = bucket + end + + bucket[#bucket + 1] = fn + return true +end + +function M.run(store, event_name, ...) + local bucket = store[event_name] + if not bucket then + return true + end + + local ok_all = true + for i = 1, #bucket do + local ok, err = pcall(bucket[i], ...) + if not ok then + ok_all = false + print(string.format("lemacs hook error [%s]: %s", event_name, tostring(err))) + end + end + + return ok_all +end + +function M.run_first_truthy(store, event_name, ...) + local bucket = store[event_name] + if not bucket then + return false + end + + for i = 1, #bucket do + local ok, result = pcall(bucket[i], ...) + if not ok then + print(string.format("lemacs hook error [%s]: %s", event_name, tostring(result))) + elseif result then + return true + end + end + + return false +end + +return M diff --git a/runtime/lua/core/modes.lua b/runtime/lua/core/modes.lua new file mode 100644 index 0000000..c0cb751 --- /dev/null +++ b/runtime/lua/core/modes.lua @@ -0,0 +1,20 @@ +local M = {} + +function M.set_mode(state, mode_name) + if mode_name ~= "normal" and mode_name ~= "insert" then + return false, "unknown mode: " .. tostring(mode_name) + end + + state.mode = mode_name + return true +end + +function M.is_insert(state) + return state.mode == "insert" +end + +function M.is_normal(state) + return state.mode == "normal" +end + +return M diff --git a/runtime/lua/core/motion.lua b/runtime/lua/core/motion.lua new file mode 100644 index 0000000..a6bcb80 --- /dev/null +++ b/runtime/lua/core/motion.lua @@ -0,0 +1,66 @@ +local M = {} + +local function is_word_char(ch) + if ch == "" then + return false + end + local byte = string.byte(ch) + if not byte then + return false + end + + if byte >= string.byte("a") and byte <= string.byte("z") then + return true + end + if byte >= string.byte("A") and byte <= string.byte("Z") then + return true + end + if byte >= string.byte("0") and byte <= string.byte("9") then + return true + end + return ch == "_" +end + +function M.forward_word() + local len = editor.buffer_length() + local point = editor.point() + if point >= len then + return + end + + local ch = editor.char_at(point) + if is_word_char(ch) then + while point < len and is_word_char(editor.char_at(point)) do + point = point + 1 + end + else + while point < len and not is_word_char(editor.char_at(point)) do + point = point + 1 + end + while point < len and is_word_char(editor.char_at(point)) do + point = point + 1 + end + end + + editor.set_point(point) +end + +function M.backward_word() + local point = editor.point() + if point <= 0 then + return + end + + point = point - 1 + while point >= 0 and not is_word_char(editor.char_at(point)) do + point = point - 1 + end + + while point >= 0 and is_word_char(editor.char_at(point)) do + point = point - 1 + end + + editor.set_point(point + 1) +end + +return M diff --git a/runtime/lua/core/state.lua b/runtime/lua/core/state.lua new file mode 100644 index 0000000..783c137 --- /dev/null +++ b/runtime/lua/core/state.lua @@ -0,0 +1,35 @@ +local M = {} + +M.commands = {} +M.keymap = {} +M.current_theme = "base16-default-light" + +M.mode_line_buffer = nil +M.message_buffer = nil +M.command_buffer = nil +M.picker_buffer = nil + +M.message_text = "ready" +M.command_mode = false +M.command_kind = "lua" +M.command_text = "" +M.mode = "normal" +M.suppress_next_text = false +M.leader_key = "space" +M.leader_pending = false +M.leader_map = {} +M.picker = { + active = false, + title = "", + query = "", + items = {}, + filtered = {}, + selected = 1, + on_select = nil, + on_cancel = nil, +} + +M.hooks = {} +M.next_anon_command_id = 1 + +return M diff --git a/runtime/lua/core/theme.lua b/runtime/lua/core/theme.lua new file mode 100644 index 0000000..cead212 --- /dev/null +++ b/runtime/lua/core/theme.lua @@ -0,0 +1,34 @@ +local M = {} + +local function rgba(hex) + local h = hex:gsub("#", "") + if #h ~= 6 then + return 0, 0, 0, 255 + end + + local r = tonumber(h:sub(1, 2), 16) or 0 + local g = tonumber(h:sub(3, 4), 16) or 0 + local b = tonumber(h:sub(5, 6), 16) or 0 + return r, g, b, 255 +end + +local default_map = { + bg_top = "base00", + bg_bottom = "base01", + text = "base05", + cursor = "base08", + accent = "base09", +} + +function M.apply_base16(palette, color_map) + local map = color_map or default_map + for editor_color, base_key in pairs(map) do + local hex = palette[base_key] + if hex then + local r, g, b, a = rgba(hex) + editor.set_theme_color(editor_color, r, g, b, a) + end + end +end + +return M diff --git a/runtime/lua/core/themes.lua b/runtime/lua/core/themes.lua new file mode 100644 index 0000000..8a76645 --- /dev/null +++ b/runtime/lua/core/themes.lua @@ -0,0 +1,8 @@ +local M = {} + +M.all = { + ["base16-default-light"] = require("themes.base16-default-light"), + ["base16-gruvbox-dark-hard"] = require("themes.base16-gruvbox-dark-hard"), +} + +return M diff --git a/runtime/lua/core/ui.lua b/runtime/lua/core/ui.lua new file mode 100644 index 0000000..9ef25d1 --- /dev/null +++ b/runtime/lua/core/ui.lua @@ -0,0 +1,84 @@ +local M = {} + +local function hex_to_rgba(hex) + local h = hex:gsub("#", "") + if #h ~= 6 then + return 0, 0, 0, 255 + end + + local r = tonumber(h:sub(1, 2), 16) or 0 + local g = tonumber(h:sub(3, 4), 16) or 0 + local b = tonumber(h:sub(5, 6), 16) or 0 + return r, g, b, 255 +end + +local function current_buffer_label() + local path = editor.buffer_path() + if not path then + return "*scratch*" + end + return path +end + +function M.update_modeline(state) + if not state.mode_line_buffer then + return + end + + local dirty = editor.buffer_modified() and "*" or "-" + local cmd_mode = string.format("[%s-cmd]", state.command_kind or "lua") + local mode = state.command_mode and cmd_mode or string.format("[%s]", state.mode or "normal") + local leader = state.leader_pending and " [leader]" or "" + local text = string.format("lemacs %s | %s | %s %s%s", dirty, current_buffer_label(), state.current_theme, mode, leader) + editor.aux_buffer_set_text(state.mode_line_buffer, text) +end + +function M.set_message(state, text) + state.message_text = text + if state.message_buffer then + editor.aux_buffer_set_text(state.message_buffer, text) + end +end + +function M.update_commandline_buffer(state) + if not state.command_buffer then + return + end + local prefix = (state.command_kind == "command") and ":" or ":lua " + editor.aux_buffer_set_text(state.command_buffer, prefix .. state.command_text) +end + +function M.setup_layout(state, palette) + local width = editor.screen_width() + local height = editor.screen_height() + local mode_h = 28 + local msg_h = 24 + local cmd_h = state.command_mode and 24 or 0 + local body_h = height - mode_h - msg_h - cmd_h + if body_h < 1 then + body_h = 1 + end + + local base00_r, base00_g, base00_b = hex_to_rgba(palette.base00) + local base01_r, base01_g, base01_b = hex_to_rgba(palette.base01) + local base02_r, base02_g, base02_b = hex_to_rgba(palette.base02) + local base03_r, base03_g, base03_b = hex_to_rgba(palette.base03) + local base05_r, base05_g, base05_b = hex_to_rgba(palette.base05) + local base06_r, base06_g, base06_b = hex_to_rgba(palette.base06) + local base09_r, base09_g, base09_b = hex_to_rgba(palette.base09) + + editor.layout_reset() + editor.layout_add_view(0, 0, 0, 0, width, body_h, true, base00_r, base00_g, base00_b, 255, base05_r, base05_g, base05_b, 255) + editor.layout_add_view(1, state.mode_line_buffer, 0, body_h, width, mode_h, false, base02_r, base02_g, base02_b, 235, base06_r, base06_g, base06_b, 255) + editor.layout_add_view(1, state.message_buffer, 0, body_h + mode_h, width, msg_h, false, base01_r, base01_g, base01_b, 240, base09_r, base09_g, base09_b, 255) + + if state.command_mode then + editor.layout_add_view(1, state.command_buffer, 0, body_h + mode_h + msg_h, width, cmd_h, false, base03_r, base03_g, base03_b, 245, base06_r, base06_g, base06_b, 255) + end +end + +function M.current_buffer_label() + return current_buffer_label() +end + +return M diff --git a/runtime/lua/themes/base16-default-light.lua b/runtime/lua/themes/base16-default-light.lua new file mode 100644 index 0000000..8f1bcb8 --- /dev/null +++ b/runtime/lua/themes/base16-default-light.lua @@ -0,0 +1,19 @@ +return { + name = "base16-default-light", + base00 = "#f8f8f8", + base01 = "#e8e8e8", + base02 = "#d8d8d8", + base03 = "#b8b8b8", + base04 = "#585858", + base05 = "#383838", + base06 = "#282828", + base07 = "#181818", + base08 = "#ab4642", + base09 = "#dc9656", + base0A = "#f7ca88", + base0B = "#a1b56c", + base0C = "#86c1b9", + base0D = "#7cafc2", + base0E = "#ba8baf", + base0F = "#a16946", +} diff --git a/runtime/lua/themes/base16-gruvbox-dark-hard.lua b/runtime/lua/themes/base16-gruvbox-dark-hard.lua new file mode 100644 index 0000000..390304d --- /dev/null +++ b/runtime/lua/themes/base16-gruvbox-dark-hard.lua @@ -0,0 +1,19 @@ +return { + name = "base16-gruvbox-dark-hard", + base00 = "#1d2021", + base01 = "#3c3836", + base02 = "#504945", + base03 = "#665c54", + base04 = "#bdae93", + base05 = "#d5c4a1", + base06 = "#ebdbb2", + base07 = "#fbf1c7", + base08 = "#fb4934", + base09 = "#fe8019", + base0A = "#fabd2f", + base0B = "#b8bb26", + base0C = "#8ec07c", + base0D = "#83a598", + base0E = "#d3869b", + base0F = "#d65d0e", +} |
