From cebb280fe546190e55e071b6adbf37cd47950f34 Mon Sep 17 00:00:00 2001 From: Collin Williams <96917990+bluedragon1221@users.noreply.github.com> Date: Mon, 11 May 2026 15:07:55 -0500 Subject: initial commit --- CMakeLists.txt | 40 ++ Makefile | 46 ++ build/lemacs | Bin 0 -> 47688 bytes build/obj/core/buffer.d | 2 + build/obj/core/buffer.o | Bin 0 -> 6456 bytes build/obj/core/editor.d | 4 + build/obj/core/editor.o | Bin 0 -> 10960 bytes build/obj/core/lua_bridge.d | 5 + build/obj/core/lua_bridge.o | Bin 0 -> 28920 bytes build/obj/main.d | 7 + build/obj/main.o | Bin 0 -> 16536 bytes init.lua | 75 +++ runtime/init.lua | 42 ++ runtime/lua/core/app.lua | 595 +++++++++++++++++++++++ runtime/lua/core/cmdline.lua | 63 +++ runtime/lua/core/commands.lua | 41 ++ runtime/lua/core/hooks.lua | 61 +++ runtime/lua/core/modes.lua | 20 + runtime/lua/core/motion.lua | 66 +++ runtime/lua/core/state.lua | 35 ++ runtime/lua/core/theme.lua | 34 ++ runtime/lua/core/themes.lua | 8 + runtime/lua/core/ui.lua | 84 ++++ runtime/lua/themes/base16-default-light.lua | 19 + runtime/lua/themes/base16-gruvbox-dark-hard.lua | 19 + shell.nix | 13 + src/core/buffer.c | 296 ++++++++++++ src/core/buffer.h | 38 ++ src/core/editor.c | 466 ++++++++++++++++++ src/core/editor.h | 96 ++++ src/core/lua_bridge.c | 611 ++++++++++++++++++++++++ src/core/lua_bridge.h | 26 + src/main.c | 429 +++++++++++++++++ test.txt | 5 + 34 files changed, 3246 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 Makefile create mode 100755 build/lemacs create mode 100644 build/obj/core/buffer.d create mode 100644 build/obj/core/buffer.o create mode 100644 build/obj/core/editor.d create mode 100644 build/obj/core/editor.o create mode 100644 build/obj/core/lua_bridge.d create mode 100644 build/obj/core/lua_bridge.o create mode 100644 build/obj/main.d create mode 100644 build/obj/main.o create mode 100644 init.lua create mode 100644 runtime/init.lua create mode 100644 runtime/lua/core/app.lua create mode 100644 runtime/lua/core/cmdline.lua create mode 100644 runtime/lua/core/commands.lua create mode 100644 runtime/lua/core/hooks.lua create mode 100644 runtime/lua/core/modes.lua create mode 100644 runtime/lua/core/motion.lua create mode 100644 runtime/lua/core/state.lua create mode 100644 runtime/lua/core/theme.lua create mode 100644 runtime/lua/core/themes.lua create mode 100644 runtime/lua/core/ui.lua create mode 100644 runtime/lua/themes/base16-default-light.lua create mode 100644 runtime/lua/themes/base16-gruvbox-dark-hard.lua create mode 100644 shell.nix create mode 100644 src/core/buffer.c create mode 100644 src/core/buffer.h create mode 100644 src/core/editor.c create mode 100644 src/core/editor.h create mode 100644 src/core/lua_bridge.c create mode 100644 src/core/lua_bridge.h create mode 100644 src/main.c create mode 100644 test.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d8cfcf6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.16) +project(lemacs C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +find_package(PkgConfig REQUIRED) + +pkg_check_modules(RAYLIB REQUIRED IMPORTED_TARGET raylib) +pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig) + +find_package(Lua 5.4 QUIET) +if(Lua_FOUND) + set(LEMACS_LUA_LIBRARIES ${LUA_LIBRARIES}) + set(LEMACS_LUA_INCLUDE_DIRS ${LUA_INCLUDE_DIR}) +else() + pkg_check_modules(LUA54 REQUIRED IMPORTED_TARGET lua5.4) +endif() + +add_executable(lemacs + src/main.c + src/core/buffer.c + src/core/editor.c + src/core/lua_bridge.c +) + +target_compile_definitions(lemacs PRIVATE LEMACS_RUNTIME_INIT="${CMAKE_SOURCE_DIR}/runtime/init.lua") + +target_include_directories(lemacs + PRIVATE + src +) + +if(Lua_FOUND) + target_include_directories(lemacs PRIVATE ${LEMACS_LUA_INCLUDE_DIRS}) + target_link_libraries(lemacs PRIVATE PkgConfig::RAYLIB PkgConfig::FONTCONFIG ${LEMACS_LUA_LIBRARIES} m) +else() + target_link_libraries(lemacs PRIVATE PkgConfig::RAYLIB PkgConfig::FONTCONFIG PkgConfig::LUA54 m) +endif() diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e6f5cda --- /dev/null +++ b/Makefile @@ -0,0 +1,46 @@ +.PHONY: build test run clean rebuild + +CC ?= gcc +PKG_CONFIG ?= pkg-config + +BUILD_DIR := build +OBJ_DIR := $(BUILD_DIR)/obj +BIN := $(BUILD_DIR)/lemacs +RUN_FILE ?= + +SRC := \ + src/main.c \ + src/core/buffer.c \ + src/core/editor.c \ + src/core/lua_bridge.c + +OBJ := $(SRC:src/%.c=$(OBJ_DIR)/%.o) +DEP := $(OBJ:.o=.d) + +CPPFLAGS += -Isrc -DLEMACS_RUNTIME_INIT=\"$(CURDIR)/runtime/init.lua\" +CFLAGS += -std=c99 -Wall -Wextra -Wpedantic -O2 -MMD -MP +LDLIBS += $(shell $(PKG_CONFIG) --libs raylib lua5.4 fontconfig) -lm +LDFLAGS += + +build: $(BIN) + +$(BIN): $(OBJ) + mkdir -p "$(BUILD_DIR)" + $(CC) $(OBJ) $(LDFLAGS) $(LDLIBS) -o "$@" + +$(OBJ_DIR)/%.o: src/%.c + mkdir -p "$(dir $@)" + $(CC) $(CPPFLAGS) $(CFLAGS) $(shell $(PKG_CONFIG) --cflags raylib lua5.4 fontconfig) -c "$<" -o "$@" + +test: build + printf '%s\n' 'No test suite is configured yet.' + +run: build + "./$(BIN)" $(RUN_FILE) + +clean: + rm -rf "$(BUILD_DIR)" + +rebuild: clean build + +-include $(DEP) diff --git a/build/lemacs b/build/lemacs new file mode 100755 index 0000000..e3cf056 Binary files /dev/null and b/build/lemacs differ diff --git a/build/obj/core/buffer.d b/build/obj/core/buffer.d new file mode 100644 index 0000000..ca3bf04 --- /dev/null +++ b/build/obj/core/buffer.d @@ -0,0 +1,2 @@ +build/obj/core/buffer.o: src/core/buffer.c src/core/buffer.h +src/core/buffer.h: diff --git a/build/obj/core/buffer.o b/build/obj/core/buffer.o new file mode 100644 index 0000000..7bedb3c Binary files /dev/null and b/build/obj/core/buffer.o differ diff --git a/build/obj/core/editor.d b/build/obj/core/editor.d new file mode 100644 index 0000000..d89e37f --- /dev/null +++ b/build/obj/core/editor.d @@ -0,0 +1,4 @@ +build/obj/core/editor.o: src/core/editor.c src/core/editor.h \ + src/core/buffer.h +src/core/editor.h: +src/core/buffer.h: diff --git a/build/obj/core/editor.o b/build/obj/core/editor.o new file mode 100644 index 0000000..1d76919 Binary files /dev/null and b/build/obj/core/editor.o differ diff --git a/build/obj/core/lua_bridge.d b/build/obj/core/lua_bridge.d new file mode 100644 index 0000000..d645b44 --- /dev/null +++ b/build/obj/core/lua_bridge.d @@ -0,0 +1,5 @@ +build/obj/core/lua_bridge.o: src/core/lua_bridge.c src/core/lua_bridge.h \ + src/core/editor.h src/core/buffer.h +src/core/lua_bridge.h: +src/core/editor.h: +src/core/buffer.h: diff --git a/build/obj/core/lua_bridge.o b/build/obj/core/lua_bridge.o new file mode 100644 index 0000000..56b65a1 Binary files /dev/null and b/build/obj/core/lua_bridge.o differ diff --git a/build/obj/main.d b/build/obj/main.d new file mode 100644 index 0000000..97ed752 --- /dev/null +++ b/build/obj/main.d @@ -0,0 +1,7 @@ +build/obj/main.o: src/main.c src/core/buffer.h src/core/editor.h \ + src/core/buffer.h src/core/lua_bridge.h src/core/editor.h +src/core/buffer.h: +src/core/editor.h: +src/core/buffer.h: +src/core/lua_bridge.h: +src/core/editor.h: diff --git a/build/obj/main.o b/build/obj/main.o new file mode 100644 index 0000000..cd63024 Binary files /dev/null and b/build/obj/main.o differ diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..9930ba7 --- /dev/null +++ b/init.lua @@ -0,0 +1,75 @@ +local open_prompt = { + active = false, + text = "", + buffer = editor.aux_buffer_new(), +} + +local function hide_open_prompt() + open_prompt.active = false + open_prompt.text = "" + editor.aux_buffer_set_text(open_prompt.buffer, "") +end + +local function show_open_prompt() + open_prompt.active = true + open_prompt.text = "" + editor.aux_buffer_set_text(open_prompt.buffer, "open file: ") +end + +local function refresh_open_prompt() + editor.aux_buffer_set_text(open_prompt.buffer, "open file: " .. open_prompt.text) +end + +lemacs.keys.bind_leader("f", function() + if not open_prompt.active then + show_open_prompt() + end +end) + +lemacs.hooks.add("intercept_key", function(key) + if not open_prompt.active then + return false + end + + if key == "escape" then + hide_open_prompt() + return true + end + + if key == "backspace" then + open_prompt.text = open_prompt.text:sub(1, #open_prompt.text - 1) + refresh_open_prompt() + return true + end + + if key == "enter" then + local path = open_prompt.text + hide_open_prompt() + if path ~= "" then + editor.open_file(path) + end + return true + end + + return open_prompt.active +end) + +lemacs.hooks.add("intercept_text", function(text) + if not open_prompt.active then + return false + end + + open_prompt.text = open_prompt.text .. text + refresh_open_prompt() + return true +end) + +lemacs.hooks.add("after_layout", function() + if not open_prompt.active then + return + end + + local width = editor.screen_width() + local y = editor.screen_height() - 24 + lemacs.ui.add_view(1, open_prompt.buffer, 0, y, width, 24, false, 42, 44, 54, 245, 232, 232, 236, 255) +end) diff --git a/runtime/init.lua b/runtime/init.lua new file mode 100644 index 0000000..5e6bd71 --- /dev/null +++ b/runtime/init.lua @@ -0,0 +1,42 @@ +local app = require("core.app") + +lemacs = lemacs or {} +lemacs.hooks = lemacs.hooks or {} + +function lemacs.hooks.add(event_name, fn) + return app.add_hook(event_name, fn) +end + +lemacs.keys = lemacs.keys or {} + +function lemacs.keys.bind(key, target) + return app.bind_key(key, target) +end + +function lemacs.keys.bind_leader(key, target) + return app.bind_leader(key, target) +end + +lemacs.ui = lemacs.ui or {} + +function lemacs.ui.add_view(source_type, source_index, x, y, width, height, show_cursor, bg_r, bg_g, bg_b, bg_a, fg_r, fg_g, fg_b, fg_a) + return editor.layout_add_view(source_type, source_index, x, y, width, height, show_cursor, bg_r, bg_g, bg_b, bg_a, fg_r, fg_g, fg_b, fg_a) +end + +app.bootstrap() + +function on_open_file(path) + return app.on_open_file(path) +end + +function on_key(key) + return app.on_key(key) +end + +function on_text(text) + return app.on_text(text) +end + +function on_tick() + app.on_tick() +end 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 ") + 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 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", +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..79ba02a --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +{pkgs ? import {}}: +pkgs.mkShell { + packages = with pkgs; [ + gnumake + pkg-config + gcc + raylib + glfw + lua5_4 + fontconfig + expat + ]; +} diff --git a/src/core/buffer.c b/src/core/buffer.c new file mode 100644 index 0000000..16a9dfe --- /dev/null +++ b/src/core/buffer.c @@ -0,0 +1,296 @@ +#include "core/buffer.h" + +#include +#include +#include + +#define INITIAL_CAPACITY 1024 + +static size_t gap_size(const Buffer *buffer) { + return buffer->gap_end - buffer->gap_start; +} + +static size_t physical_index(const Buffer *buffer, size_t logical) { + if (logical < buffer->gap_start) { + return logical; + } + return logical + gap_size(buffer); +} + +static bool ensure_gap(Buffer *buffer, size_t needed) { + if (gap_size(buffer) >= needed) { + return true; + } + + size_t old_gap = gap_size(buffer); + size_t old_capacity = buffer->capacity; + size_t new_capacity = old_capacity; + while ((new_capacity - buffer->gap_start - (old_capacity - buffer->gap_end)) < needed) { + new_capacity *= 2; + } + + char *new_data = (char *)malloc(new_capacity); + if (!new_data) { + return false; + } + + memcpy(new_data, buffer->data, buffer->gap_start); + size_t tail_size = old_capacity - buffer->gap_end; + size_t new_gap_end = new_capacity - tail_size; + memcpy(new_data + new_gap_end, buffer->data + buffer->gap_end, tail_size); + + free(buffer->data); + buffer->data = new_data; + buffer->capacity = new_capacity; + buffer->gap_end = new_gap_end; + + (void)old_gap; + return true; +} + +static void move_gap(Buffer *buffer, size_t target) { + if (target == buffer->gap_start) { + return; + } + + if (target < buffer->gap_start) { + size_t move = buffer->gap_start - target; + memmove(buffer->data + buffer->gap_end - move, buffer->data + target, move); + buffer->gap_start = target; + buffer->gap_end -= move; + return; + } + + size_t move = target - buffer->gap_start; + memmove(buffer->data + buffer->gap_start, buffer->data + buffer->gap_end, move); + buffer->gap_start += move; + buffer->gap_end += move; +} + +bool buffer_init(Buffer *buffer) { + buffer->data = (char *)malloc(INITIAL_CAPACITY); + if (!buffer->data) { + return false; + } + buffer->capacity = INITIAL_CAPACITY; + buffer->gap_start = 0; + buffer->gap_end = INITIAL_CAPACITY; + buffer->path[0] = '\0'; + buffer->modified = false; + return true; +} + +void buffer_free(Buffer *buffer) { + free(buffer->data); + buffer->data = NULL; + buffer->capacity = 0; + buffer->gap_start = 0; + buffer->gap_end = 0; +} + +size_t buffer_length(const Buffer *buffer) { + return buffer->capacity - gap_size(buffer); +} + +size_t buffer_point(const Buffer *buffer) { + return buffer->gap_start; +} + +void buffer_set_point(Buffer *buffer, size_t point) { + size_t len = buffer_length(buffer); + if (point > len) { + point = len; + } + move_gap(buffer, point); +} + +bool buffer_insert_char(Buffer *buffer, char c) { + if (!ensure_gap(buffer, 1)) { + return false; + } + buffer->data[buffer->gap_start++] = c; + buffer->modified = true; + return true; +} + +bool buffer_insert_cstr(Buffer *buffer, const char *text) { + size_t len = strlen(text); + if (!ensure_gap(buffer, len)) { + return false; + } + memcpy(buffer->data + buffer->gap_start, text, len); + buffer->gap_start += len; + if (len > 0) { + buffer->modified = true; + } + return true; +} + +bool buffer_backspace(Buffer *buffer) { + if (buffer->gap_start == 0) { + return false; + } + buffer->gap_start--; + buffer->modified = true; + return true; +} + +bool buffer_delete_forward(Buffer *buffer) { + if (buffer->gap_end >= buffer->capacity) { + return false; + } + buffer->gap_end++; + buffer->modified = true; + return true; +} + +char buffer_char_at(const Buffer *buffer, size_t index) { + size_t len = buffer_length(buffer); + if (index >= len) { + return '\0'; + } + return buffer->data[physical_index(buffer, index)]; +} + +bool buffer_load_file(Buffer *buffer, const char *path) { + FILE *fp = fopen(path, "rb"); + if (!fp) { + return false; + } + + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return false; + } + + long file_size = ftell(fp); + if (file_size < 0) { + fclose(fp); + return false; + } + rewind(fp); + + buffer->gap_start = 0; + buffer->gap_end = buffer->capacity; + + if (!ensure_gap(buffer, (size_t)file_size + 1)) { + fclose(fp); + return false; + } + + size_t read = fread(buffer->data, 1, (size_t)file_size, fp); + fclose(fp); + if (read != (size_t)file_size) { + return false; + } + + buffer->gap_start = (size_t)file_size; + buffer->gap_end = buffer->capacity; + + strncpy(buffer->path, path, sizeof(buffer->path) - 1); + buffer->path[sizeof(buffer->path) - 1] = '\0'; + buffer->modified = false; + return true; +} + +bool buffer_save_file(Buffer *buffer, const char *path) { + const char *out_path = path; + if (!out_path || out_path[0] == '\0') { + out_path = buffer->path; + } + + if (!out_path || out_path[0] == '\0') { + return false; + } + + FILE *fp = fopen(out_path, "wb"); + if (!fp) { + return false; + } + + size_t before = buffer->gap_start; + size_t after = buffer->capacity - buffer->gap_end; + + if (before > 0 && fwrite(buffer->data, 1, before, fp) != before) { + fclose(fp); + return false; + } + + if (after > 0 && fwrite(buffer->data + buffer->gap_end, 1, after, fp) != after) { + fclose(fp); + return false; + } + + fclose(fp); + + if (path && path[0] != '\0') { + strncpy(buffer->path, path, sizeof(buffer->path) - 1); + buffer->path[sizeof(buffer->path) - 1] = '\0'; + } + + buffer->modified = false; + + return true; +} + +const char *buffer_path(const Buffer *buffer) { + return buffer->path; +} + +bool buffer_modified(const Buffer *buffer) { + return buffer->modified; +} + +void buffer_index_to_line_col(const Buffer *buffer, size_t index, int *line, int *col) { + size_t len = buffer_length(buffer); + if (index > len) { + index = len; + } + + int l = 0; + int c = 0; + for (size_t i = 0; i < index; ++i) { + char ch = buffer_char_at(buffer, i); + if (ch == '\n') { + l++; + c = 0; + } else { + c++; + } + } + + *line = l; + *col = c; +} + +size_t buffer_line_col_to_index(const Buffer *buffer, int target_line, int target_col) { + if (target_line < 0) { + target_line = 0; + } + if (target_col < 0) { + target_col = 0; + } + + size_t len = buffer_length(buffer); + int line = 0; + int col = 0; + + for (size_t i = 0; i < len; ++i) { + if (line == target_line && col == target_col) { + return i; + } + + char ch = buffer_char_at(buffer, i); + if (ch == '\n') { + if (line == target_line) { + return i; + } + line++; + col = 0; + } else { + col++; + } + } + + return len; +} diff --git a/src/core/buffer.h b/src/core/buffer.h new file mode 100644 index 0000000..8fac8ab --- /dev/null +++ b/src/core/buffer.h @@ -0,0 +1,38 @@ +#ifndef LEMACS_BUFFER_H +#define LEMACS_BUFFER_H + +#include +#include + +typedef struct { + char *data; + size_t capacity; + size_t gap_start; + size_t gap_end; + char path[1024]; + bool modified; +} Buffer; + +bool buffer_init(Buffer *buffer); +void buffer_free(Buffer *buffer); + +size_t buffer_length(const Buffer *buffer); +size_t buffer_point(const Buffer *buffer); +void buffer_set_point(Buffer *buffer, size_t point); + +bool buffer_insert_char(Buffer *buffer, char c); +bool buffer_insert_cstr(Buffer *buffer, const char *text); +bool buffer_backspace(Buffer *buffer); +bool buffer_delete_forward(Buffer *buffer); + +char buffer_char_at(const Buffer *buffer, size_t index); + +bool buffer_load_file(Buffer *buffer, const char *path); +bool buffer_save_file(Buffer *buffer, const char *path); +const char *buffer_path(const Buffer *buffer); +bool buffer_modified(const Buffer *buffer); + +void buffer_index_to_line_col(const Buffer *buffer, size_t index, int *line, int *col); +size_t buffer_line_col_to_index(const Buffer *buffer, int target_line, int target_col); + +#endif diff --git a/src/core/editor.c b/src/core/editor.c new file mode 100644 index 0000000..43c28c7 --- /dev/null +++ b/src/core/editor.c @@ -0,0 +1,466 @@ +#include "core/editor.h" + +#include +#include +#include + +static EditorColor make_color(int r, int g, int b, int a) { + if (r < 0) r = 0; + if (g < 0) g = 0; + if (b < 0) b = 0; + if (a < 0) a = 0; + if (r > 255) r = 255; + if (g > 255) g = 255; + if (b > 255) b = 255; + if (a > 255) a = 255; + + EditorColor c; + c.r = (uint8_t)r; + c.g = (uint8_t)g; + c.b = (uint8_t)b; + c.a = (uint8_t)a; + return c; +} + +static Buffer *active_buffer(Editor *editor) { + if (editor->current_buffer < 0 || editor->current_buffer >= editor->buffer_count) { + return NULL; + } + return &editor->buffers[editor->current_buffer]; +} + +static const Buffer *active_buffer_const(const Editor *editor) { + if (editor->current_buffer < 0 || editor->current_buffer >= editor->buffer_count) { + return NULL; + } + return &editor->buffers[editor->current_buffer]; +} + +static void clamp_point(Editor *editor) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + size_t point = buffer_point(buffer); + size_t len = buffer_length(buffer); + if (point > len) { + buffer_set_point(buffer, len); + } +} + +static bool valid_aux_index(const Editor *editor, int aux_index) { + return aux_index >= 0 && aux_index < editor->aux_buffer_count; +} + +static bool valid_buffer_index(const Editor *editor, int index) { + return index >= 0 && index < editor->buffer_count; +} + +bool editor_init(Editor *editor) { + editor->buffer_count = 0; + editor->current_buffer = 0; + + if (!buffer_init(&editor->buffers[0])) { + return false; + } + + editor->buffer_count = 1; + editor->aux_buffer_count = 0; + editor->desired_col = -1; + editor->scroll_line = 0; + snprintf(editor->font_query, sizeof(editor->font_query), "%s", "monospace:size=14"); + editor->font_dirty = true; + editor->bg_top = make_color(248, 248, 248, 255); + editor->bg_bottom = make_color(238, 238, 238, 255); + editor->text = make_color(56, 60, 67, 255); + editor->cursor = make_color(176, 65, 11, 255); + editor->cursor_shape = LEMACS_CURSOR_BAR; + editor->cursor_thickness = 2; + editor->view_count = 0; + editor->should_quit = false; + return true; +} + +void editor_free(Editor *editor) { + for (int i = 0; i < editor->buffer_count; ++i) { + buffer_free(&editor->buffers[i]); + } + + for (int i = 0; i < editor->aux_buffer_count; ++i) { + buffer_free(&editor->aux_buffers[i]); + } + + editor->buffer_count = 0; + editor->current_buffer = 0; + editor->aux_buffer_count = 0; + editor->view_count = 0; +} + +void editor_set_font_query(Editor *editor, const char *query) { + snprintf(editor->font_query, sizeof(editor->font_query), "%s", (query && query[0] != '\0') ? query : "monospace:size=14"); + editor->font_dirty = true; +} + +const char *editor_font_query(const Editor *editor) { + return editor->font_query; +} + +bool editor_consume_font_dirty(Editor *editor) { + bool dirty = editor->font_dirty; + editor->font_dirty = false; + return dirty; +} + +bool editor_set_theme_color(Editor *editor, const char *name, int r, int g, int b, int a) { + EditorColor c = make_color(r, g, b, a); + + if (strcmp(name, "bg_top") == 0) { + editor->bg_top = c; + return true; + } + if (strcmp(name, "bg_bottom") == 0) { + editor->bg_bottom = c; + return true; + } + if (strcmp(name, "text") == 0) { + editor->text = c; + return true; + } + if (strcmp(name, "cursor") == 0) { + editor->cursor = c; + return true; + } + if (strcmp(name, "accent") == 0) { + editor->cursor = c; + return true; + } + + return false; +} + +bool editor_set_cursor_shape(Editor *editor, int shape) { + if (shape < LEMACS_CURSOR_BAR || shape > LEMACS_CURSOR_UNDERLINE) { + return false; + } + editor->cursor_shape = shape; + return true; +} + +void editor_set_cursor_thickness(Editor *editor, int thickness) { + if (thickness < 1) { + thickness = 1; + } + if (thickness > 24) { + thickness = 24; + } + editor->cursor_thickness = thickness; +} + +int editor_cursor_shape(const Editor *editor) { + return editor->cursor_shape; +} + +int editor_cursor_thickness(const Editor *editor) { + return editor->cursor_thickness; +} + +int editor_aux_buffer_new(Editor *editor) { + if (editor->aux_buffer_count >= LEMACS_MAX_AUX_BUFFERS) { + return -1; + } + + int index = editor->aux_buffer_count; + if (!buffer_init(&editor->aux_buffers[index])) { + return -1; + } + + editor->aux_buffer_count++; + return index; +} + +bool editor_aux_buffer_set_text(Editor *editor, int aux_index, const char *text) { + if (!valid_aux_index(editor, aux_index)) { + return false; + } + + Buffer *buffer = &editor->aux_buffers[aux_index]; + buffer->gap_start = 0; + buffer->gap_end = buffer->capacity; + buffer->path[0] = '\0'; + buffer->modified = false; + + if (!text || text[0] == '\0') { + return true; + } + + return buffer_insert_cstr(buffer, text); +} + +Buffer *editor_aux_buffer_get(Editor *editor, int aux_index) { + if (!valid_aux_index(editor, aux_index)) { + return NULL; + } + return &editor->aux_buffers[aux_index]; +} + +void editor_layout_reset(Editor *editor) { + editor->view_count = 0; +} + +bool editor_layout_add_view(Editor *editor, int source_type, int source_index, int x, int y, int width, int height, bool show_cursor, EditorColor bg, EditorColor fg) { + if (editor->view_count >= LEMACS_MAX_VIEWS) { + return false; + } + + if (width <= 0 || height <= 0) { + return false; + } + + if (source_type == 1 && !valid_aux_index(editor, source_index)) { + return false; + } + + if (source_type != 0 && source_type != 1) { + return false; + } + + EditorView *view = &editor->views[editor->view_count++]; + view->x = x; + view->y = y; + view->width = width; + view->height = height; + view->source_type = source_type; + view->source_index = source_index; + view->show_cursor = show_cursor; + view->bg = bg; + view->fg = fg; + return true; +} + +void editor_move_left(Editor *editor) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + size_t point = buffer_point(buffer); + if (point > 0) { + buffer_set_point(buffer, point - 1); + } + editor->desired_col = -1; +} + +void editor_move_right(Editor *editor) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + size_t point = buffer_point(buffer); + size_t len = buffer_length(buffer); + if (point < len) { + buffer_set_point(buffer, point + 1); + } + editor->desired_col = -1; +} + +void editor_move_up(Editor *editor) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + size_t point = buffer_point(buffer); + int line = 0; + int col = 0; + buffer_index_to_line_col(buffer, point, &line, &col); + if (editor->desired_col < 0) { + editor->desired_col = col; + } + if (line > 0) { + size_t new_point = buffer_line_col_to_index(buffer, line - 1, editor->desired_col); + buffer_set_point(buffer, new_point); + } + clamp_point(editor); +} + +void editor_move_down(Editor *editor) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + size_t point = buffer_point(buffer); + int line = 0; + int col = 0; + buffer_index_to_line_col(buffer, point, &line, &col); + if (editor->desired_col < 0) { + editor->desired_col = col; + } + + size_t len = buffer_length(buffer); + int last_line = 0; + int last_col = 0; + buffer_index_to_line_col(buffer, len, &last_line, &last_col); + if (line < last_line) { + size_t new_point = buffer_line_col_to_index(buffer, line + 1, editor->desired_col); + buffer_set_point(buffer, new_point); + } + clamp_point(editor); +} + +void editor_move_beginning_of_line(Editor *editor) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + size_t point = buffer_point(buffer); + while (point > 0) { + char ch = buffer_char_at(buffer, point - 1); + if (ch == '\n') { + break; + } + point--; + } + buffer_set_point(buffer, point); + editor->desired_col = -1; +} + +void editor_move_end_of_line(Editor *editor) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + size_t point = buffer_point(buffer); + size_t len = buffer_length(buffer); + while (point < len) { + char ch = buffer_char_at(buffer, point); + if (ch == '\n') { + break; + } + point++; + } + buffer_set_point(buffer, point); + editor->desired_col = -1; +} + +void editor_set_point(Editor *editor, size_t point) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return; + } + + buffer_set_point(buffer, point); + editor->desired_col = -1; +} + +char editor_char_at(const Editor *editor, size_t index) { + const Buffer *buffer = active_buffer_const(editor); + if (!buffer) { + return '\0'; + } + return buffer_char_at(buffer, index); +} + +bool editor_open_file(Editor *editor, const char *path) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return false; + } + + if (!path || path[0] == '\0') { + return false; + } + + if (!buffer_load_file(buffer, path)) { + if (errno != ENOENT) { + return false; + } + + buffer->gap_start = 0; + buffer->gap_end = buffer->capacity; + buffer->modified = false; + strncpy(buffer->path, path, sizeof(buffer->path) - 1); + buffer->path[sizeof(buffer->path) - 1] = '\0'; + } + + buffer_set_point(buffer, 0); + editor->desired_col = -1; + editor->scroll_line = 0; + + return true; +} + +bool editor_save_file(Editor *editor, const char *path) { + Buffer *buffer = active_buffer(editor); + if (!buffer) { + return false; + } + return buffer_save_file(buffer, path); +} + +Buffer *editor_current_buffer(Editor *editor) { + return active_buffer(editor); +} + +const Buffer *editor_current_buffer_const(const Editor *editor) { + return active_buffer_const(editor); +} + +int editor_buffer_count(const Editor *editor) { + return editor->buffer_count; +} + +int editor_current_buffer_index(const Editor *editor) { + return editor->current_buffer; +} + +bool editor_switch_buffer(Editor *editor, int index) { + if (!valid_buffer_index(editor, index)) { + return false; + } + + editor->current_buffer = index; + editor->desired_col = -1; + editor->scroll_line = 0; + return true; +} + +int editor_new_buffer(Editor *editor) { + if (editor->buffer_count >= LEMACS_MAX_BUFFERS) { + return -1; + } + + int index = editor->buffer_count; + if (!buffer_init(&editor->buffers[index])) { + return -1; + } + + editor->buffer_count++; + editor->current_buffer = index; + editor->desired_col = -1; + editor->scroll_line = 0; + return index; +} + +const char *editor_buffer_path_at(const Editor *editor, int index) { + if (!valid_buffer_index(editor, index)) { + return NULL; + } + + const char *path = buffer_path(&editor->buffers[index]); + if (!path || path[0] == '\0') { + return NULL; + } + return path; +} + +bool editor_buffer_modified_at(const Editor *editor, int index) { + if (!valid_buffer_index(editor, index)) { + return false; + } + return buffer_modified(&editor->buffers[index]); +} diff --git a/src/core/editor.h b/src/core/editor.h new file mode 100644 index 0000000..5388197 --- /dev/null +++ b/src/core/editor.h @@ -0,0 +1,96 @@ +#ifndef LEMACS_EDITOR_H +#define LEMACS_EDITOR_H + +#include +#include + +#include "core/buffer.h" + +#define LEMACS_MAX_AUX_BUFFERS 8 +#define LEMACS_MAX_VIEWS 16 +#define LEMACS_MAX_BUFFERS 32 + +#define LEMACS_CURSOR_BAR 0 +#define LEMACS_CURSOR_BLOCK 1 +#define LEMACS_CURSOR_UNDERLINE 2 + +typedef struct { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; +} EditorColor; + +typedef struct { + int x; + int y; + int width; + int height; + int source_type; + int source_index; + bool show_cursor; + EditorColor bg; + EditorColor fg; +} EditorView; + +typedef struct { + Buffer buffers[LEMACS_MAX_BUFFERS]; + int buffer_count; + int current_buffer; + Buffer aux_buffers[LEMACS_MAX_AUX_BUFFERS]; + int aux_buffer_count; + int desired_col; + int scroll_line; + char font_query[256]; + bool font_dirty; + EditorView views[LEMACS_MAX_VIEWS]; + int view_count; + EditorColor bg_top; + EditorColor bg_bottom; + EditorColor text; + EditorColor cursor; + int cursor_shape; + int cursor_thickness; + bool should_quit; +} Editor; + +bool editor_init(Editor *editor); +void editor_free(Editor *editor); + +void editor_set_font_query(Editor *editor, const char *query); +const char *editor_font_query(const Editor *editor); +bool editor_consume_font_dirty(Editor *editor); +bool editor_set_theme_color(Editor *editor, const char *name, int r, int g, int b, int a); +bool editor_set_cursor_shape(Editor *editor, int shape); +void editor_set_cursor_thickness(Editor *editor, int thickness); +int editor_cursor_shape(const Editor *editor); +int editor_cursor_thickness(const Editor *editor); + +int editor_aux_buffer_new(Editor *editor); +bool editor_aux_buffer_set_text(Editor *editor, int aux_index, const char *text); +Buffer *editor_aux_buffer_get(Editor *editor, int aux_index); + +void editor_layout_reset(Editor *editor); +bool editor_layout_add_view(Editor *editor, int source_type, int source_index, int x, int y, int width, int height, bool show_cursor, EditorColor bg, EditorColor fg); + +void editor_move_left(Editor *editor); +void editor_move_right(Editor *editor); +void editor_move_up(Editor *editor); +void editor_move_down(Editor *editor); +void editor_move_beginning_of_line(Editor *editor); +void editor_move_end_of_line(Editor *editor); +void editor_set_point(Editor *editor, size_t point); +char editor_char_at(const Editor *editor, size_t index); + +bool editor_open_file(Editor *editor, const char *path); +bool editor_save_file(Editor *editor, const char *path); +Buffer *editor_current_buffer(Editor *editor); +const Buffer *editor_current_buffer_const(const Editor *editor); +int editor_buffer_count(const Editor *editor); +int editor_current_buffer_index(const Editor *editor); +bool editor_switch_buffer(Editor *editor, int index); +int editor_new_buffer(Editor *editor); +const char *editor_buffer_path_at(const Editor *editor, int index); +bool editor_buffer_modified_at(const Editor *editor, int index); + +#endif diff --git a/src/core/lua_bridge.c b/src/core/lua_bridge.c new file mode 100644 index 0000000..f1bf16c --- /dev/null +++ b/src/core/lua_bridge.c @@ -0,0 +1,611 @@ +#include "core/lua_bridge.h" + +#include +#include + +#include + +#include +#include +#include + +static const char *EDITOR_REG_KEY = "lemacs.editor"; + +static Editor *get_editor(lua_State *L) { + lua_getfield(L, LUA_REGISTRYINDEX, EDITOR_REG_KEY); + Editor *editor = (Editor *)lua_touserdata(L, -1); + lua_pop(L, 1); + return editor; +} + +static int l_insert(lua_State *L) { + Editor *editor = get_editor(L); + Buffer *buffer = editor_current_buffer(editor); + if (!buffer) { + lua_pushboolean(L, 0); + return 1; + } + + size_t len = 0; + const char *text = luaL_checklstring(L, 1, &len); + for (size_t i = 0; i < len; ++i) { + if (!buffer_insert_char(buffer, text[i])) { + lua_pushboolean(L, 0); + return 1; + } + } + lua_pushboolean(L, 1); + return 1; +} + +static int l_backspace(lua_State *L) { + Editor *editor = get_editor(L); + Buffer *buffer = editor_current_buffer(editor); + lua_pushboolean(L, buffer ? buffer_backspace(buffer) : false); + return 1; +} + +static int l_delete_forward(lua_State *L) { + Editor *editor = get_editor(L); + Buffer *buffer = editor_current_buffer(editor); + lua_pushboolean(L, buffer ? buffer_delete_forward(buffer) : false); + return 1; +} + +static int l_move_left(lua_State *L) { + Editor *editor = get_editor(L); + editor_move_left(editor); + return 0; +} + +static int l_move_right(lua_State *L) { + Editor *editor = get_editor(L); + editor_move_right(editor); + return 0; +} + +static int l_move_up(lua_State *L) { + Editor *editor = get_editor(L); + editor_move_up(editor); + return 0; +} + +static int l_move_down(lua_State *L) { + Editor *editor = get_editor(L); + editor_move_down(editor); + return 0; +} + +static int l_move_beginning_of_line(lua_State *L) { + Editor *editor = get_editor(L); + editor_move_beginning_of_line(editor); + return 0; +} + +static int l_move_end_of_line(lua_State *L) { + Editor *editor = get_editor(L); + editor_move_end_of_line(editor); + return 0; +} + +static int l_open_file(lua_State *L) { + Editor *editor = get_editor(L); + const char *path = luaL_checkstring(L, 1); + lua_pushboolean(L, editor_open_file(editor, path)); + return 1; +} + +static int l_save_file(lua_State *L) { + Editor *editor = get_editor(L); + const char *path = NULL; + if (!lua_isnoneornil(L, 1)) { + path = luaL_checkstring(L, 1); + } + lua_pushboolean(L, editor_save_file(editor, path)); + return 1; +} + +static int l_get_point(lua_State *L) { + Editor *editor = get_editor(L); + Buffer *buffer = editor_current_buffer(editor); + lua_pushinteger(L, (lua_Integer)(buffer ? buffer_point(buffer) : 0)); + return 1; +} + +static int l_set_point(lua_State *L) { + Editor *editor = get_editor(L); + lua_Integer point = luaL_checkinteger(L, 1); + if (point < 0) { + point = 0; + } + editor_set_point(editor, (size_t)point); + return 0; +} + +static int l_char_at(lua_State *L) { + Editor *editor = get_editor(L); + lua_Integer idx = luaL_checkinteger(L, 1); + if (idx < 0) { + lua_pushstring(L, ""); + return 1; + } + + char ch = editor_char_at(editor, (size_t)idx); + if (ch == '\0') { + lua_pushstring(L, ""); + return 1; + } + + char text[2] = {ch, '\0'}; + lua_pushstring(L, text); + return 1; +} + +static int l_set_font(lua_State *L) { + Editor *editor = get_editor(L); + const char *query = luaL_checkstring(L, 1); + editor_set_font_query(editor, query); + return 0; +} + +static int l_set_theme_color(lua_State *L) { + Editor *editor = get_editor(L); + const char *name = luaL_checkstring(L, 1); + int r = (int)luaL_checkinteger(L, 2); + int g = (int)luaL_checkinteger(L, 3); + int b = (int)luaL_checkinteger(L, 4); + int a = 255; + if (!lua_isnoneornil(L, 5)) { + a = (int)luaL_checkinteger(L, 5); + } + + lua_pushboolean(L, editor_set_theme_color(editor, name, r, g, b, a)); + return 1; +} + +static int l_set_cursor_shape(lua_State *L) { + Editor *editor = get_editor(L); + const char *shape = luaL_checkstring(L, 1); + + int cursor_shape = -1; + if (strcmp(shape, "bar") == 0) { + cursor_shape = LEMACS_CURSOR_BAR; + } else if (strcmp(shape, "block") == 0) { + cursor_shape = LEMACS_CURSOR_BLOCK; + } else if (strcmp(shape, "underline") == 0) { + cursor_shape = LEMACS_CURSOR_UNDERLINE; + } else { + lua_pushboolean(L, 0); + return 1; + } + + lua_pushboolean(L, editor_set_cursor_shape(editor, cursor_shape)); + return 1; +} + +static int l_set_cursor_thickness(lua_State *L) { + Editor *editor = get_editor(L); + int thickness = (int)luaL_checkinteger(L, 1); + editor_set_cursor_thickness(editor, thickness); + return 0; +} + +static int l_aux_buffer_new(lua_State *L) { + Editor *editor = get_editor(L); + int index = editor_aux_buffer_new(editor); + if (index < 0) { + lua_pushnil(L); + return 1; + } + lua_pushinteger(L, index); + return 1; +} + +static int l_aux_buffer_set_text(lua_State *L) { + Editor *editor = get_editor(L); + int aux_index = (int)luaL_checkinteger(L, 1); + const char *text = luaL_checkstring(L, 2); + lua_pushboolean(L, editor_aux_buffer_set_text(editor, aux_index, text)); + return 1; +} + +static int l_layout_reset(lua_State *L) { + Editor *editor = get_editor(L); + editor_layout_reset(editor); + return 0; +} + +static int l_layout_add_view(lua_State *L) { + Editor *editor = get_editor(L); + int source_type = (int)luaL_checkinteger(L, 1); + int source_index = (int)luaL_checkinteger(L, 2); + int x = (int)luaL_checkinteger(L, 3); + int y = (int)luaL_checkinteger(L, 4); + int width = (int)luaL_checkinteger(L, 5); + int height = (int)luaL_checkinteger(L, 6); + bool show_cursor = lua_toboolean(L, 7) != 0; + int bg_r = (int)luaL_checkinteger(L, 8); + int bg_g = (int)luaL_checkinteger(L, 9); + int bg_b = (int)luaL_checkinteger(L, 10); + int bg_a = (int)luaL_optinteger(L, 11, 255); + int fg_r = (int)luaL_checkinteger(L, 12); + int fg_g = (int)luaL_checkinteger(L, 13); + int fg_b = (int)luaL_checkinteger(L, 14); + int fg_a = (int)luaL_optinteger(L, 15, 255); + + EditorColor bg = {(uint8_t)bg_r, (uint8_t)bg_g, (uint8_t)bg_b, (uint8_t)bg_a}; + EditorColor fg = {(uint8_t)fg_r, (uint8_t)fg_g, (uint8_t)fg_b, (uint8_t)fg_a}; + + lua_pushboolean(L, editor_layout_add_view(editor, source_type, source_index, x, y, width, height, show_cursor, bg, fg)); + return 1; +} + +static int l_screen_width(lua_State *L) { + lua_pushinteger(L, GetScreenWidth()); + return 1; +} + +static int l_screen_height(lua_State *L) { + lua_pushinteger(L, GetScreenHeight()); + return 1; +} + +static int l_buffer_length(lua_State *L) { + Editor *editor = get_editor(L); + Buffer *buffer = editor_current_buffer(editor); + lua_pushinteger(L, (lua_Integer)(buffer ? buffer_length(buffer) : 0)); + return 1; +} + +static int l_request_quit(lua_State *L) { + Editor *editor = get_editor(L); + editor->should_quit = true; + return 0; +} + +static int l_buffer_path(lua_State *L) { + Editor *editor = get_editor(L); + Buffer *buffer = editor_current_buffer(editor); + const char *path = buffer ? buffer_path(buffer) : NULL; + if (path && path[0] != '\0') { + lua_pushstring(L, path); + } else { + lua_pushnil(L); + } + return 1; +} + +static int l_buffer_modified(lua_State *L) { + Editor *editor = get_editor(L); + Buffer *buffer = editor_current_buffer(editor); + lua_pushboolean(L, buffer ? buffer_modified(buffer) : false); + return 1; +} + +static int l_buffer_count(lua_State *L) { + Editor *editor = get_editor(L); + lua_pushinteger(L, (lua_Integer)editor_buffer_count(editor)); + return 1; +} + +static int l_current_buffer_index(lua_State *L) { + Editor *editor = get_editor(L); + lua_pushinteger(L, (lua_Integer)editor_current_buffer_index(editor)); + return 1; +} + +static int l_switch_buffer(lua_State *L) { + Editor *editor = get_editor(L); + int index = (int)luaL_checkinteger(L, 1); + lua_pushboolean(L, editor_switch_buffer(editor, index)); + return 1; +} + +static int l_new_buffer(lua_State *L) { + Editor *editor = get_editor(L); + int index = editor_new_buffer(editor); + if (index < 0) { + lua_pushnil(L); + } else { + lua_pushinteger(L, index); + } + return 1; +} + +static int l_buffer_path_at(lua_State *L) { + Editor *editor = get_editor(L); + int index = (int)luaL_checkinteger(L, 1); + const char *path = editor_buffer_path_at(editor, index); + if (!path) { + lua_pushnil(L); + } else { + lua_pushstring(L, path); + } + return 1; +} + +static int l_buffer_modified_at(lua_State *L) { + Editor *editor = get_editor(L); + int index = (int)luaL_checkinteger(L, 1); + lua_pushboolean(L, editor_buffer_modified_at(editor, index)); + return 1; +} + +static void register_editor_api(lua_State *L) { + lua_newtable(L); + + lua_pushcfunction(L, l_insert); + lua_setfield(L, -2, "insert"); + + lua_pushcfunction(L, l_backspace); + lua_setfield(L, -2, "backspace"); + + lua_pushcfunction(L, l_delete_forward); + lua_setfield(L, -2, "delete_forward"); + + lua_pushcfunction(L, l_move_left); + lua_setfield(L, -2, "move_left"); + + lua_pushcfunction(L, l_move_right); + lua_setfield(L, -2, "move_right"); + + lua_pushcfunction(L, l_move_up); + lua_setfield(L, -2, "move_up"); + + lua_pushcfunction(L, l_move_down); + lua_setfield(L, -2, "move_down"); + + lua_pushcfunction(L, l_move_beginning_of_line); + lua_setfield(L, -2, "move_beginning_of_line"); + + lua_pushcfunction(L, l_move_end_of_line); + lua_setfield(L, -2, "move_end_of_line"); + + lua_pushcfunction(L, l_open_file); + lua_setfield(L, -2, "open_file"); + + lua_pushcfunction(L, l_save_file); + lua_setfield(L, -2, "save_file"); + + lua_pushcfunction(L, l_aux_buffer_new); + lua_setfield(L, -2, "aux_buffer_new"); + + lua_pushcfunction(L, l_aux_buffer_set_text); + lua_setfield(L, -2, "aux_buffer_set_text"); + + lua_pushcfunction(L, l_layout_reset); + lua_setfield(L, -2, "layout_reset"); + + lua_pushcfunction(L, l_layout_add_view); + lua_setfield(L, -2, "layout_add_view"); + + lua_pushcfunction(L, l_screen_width); + lua_setfield(L, -2, "screen_width"); + + lua_pushcfunction(L, l_screen_height); + lua_setfield(L, -2, "screen_height"); + + lua_pushcfunction(L, l_get_point); + lua_setfield(L, -2, "point"); + + lua_pushcfunction(L, l_set_point); + lua_setfield(L, -2, "set_point"); + + lua_pushcfunction(L, l_char_at); + lua_setfield(L, -2, "char_at"); + + lua_pushcfunction(L, l_set_font); + lua_setfield(L, -2, "set_font"); + + lua_pushcfunction(L, l_set_theme_color); + lua_setfield(L, -2, "set_theme_color"); + + lua_pushcfunction(L, l_set_cursor_shape); + lua_setfield(L, -2, "set_cursor_shape"); + + lua_pushcfunction(L, l_set_cursor_thickness); + lua_setfield(L, -2, "set_cursor_thickness"); + + lua_pushcfunction(L, l_buffer_length); + lua_setfield(L, -2, "buffer_length"); + + lua_pushcfunction(L, l_buffer_path); + lua_setfield(L, -2, "buffer_path"); + + lua_pushcfunction(L, l_buffer_modified); + lua_setfield(L, -2, "buffer_modified"); + + lua_pushcfunction(L, l_buffer_count); + lua_setfield(L, -2, "buffer_count"); + + lua_pushcfunction(L, l_current_buffer_index); + lua_setfield(L, -2, "current_buffer_index"); + + lua_pushcfunction(L, l_switch_buffer); + lua_setfield(L, -2, "switch_buffer"); + + lua_pushcfunction(L, l_new_buffer); + lua_setfield(L, -2, "new_buffer"); + + lua_pushcfunction(L, l_buffer_path_at); + lua_setfield(L, -2, "buffer_path_at"); + + lua_pushcfunction(L, l_buffer_modified_at); + lua_setfield(L, -2, "buffer_modified_at"); + + lua_pushcfunction(L, l_request_quit); + lua_setfield(L, -2, "request_quit"); + + lua_setglobal(L, "editor"); +} + +static bool call_global_with_string(lua_State *L, const char *name, const char *arg) { + lua_getglobal(L, name); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 1); + return false; + } + + lua_pushstring(L, arg); + if (lua_pcall(L, 1, 1, 0) != LUA_OK) { + fprintf(stderr, "lua error in %s: %s\n", name, lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + + bool handled = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + return handled; +} + +static bool call_global_noargs(lua_State *L, const char *name) { + lua_getglobal(L, name); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 1); + return false; + } + + if (lua_pcall(L, 0, 0, 0) != LUA_OK) { + fprintf(stderr, "lua error in %s: %s\n", name, lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + + return true; +} + +static void configure_lua_package_path(lua_State *L, const char *init_script_path) { + char runtime_lua_path[2048] = {0}; + snprintf(runtime_lua_path, sizeof(runtime_lua_path), "%s", init_script_path ? init_script_path : "runtime/init.lua"); + + char *last_slash = strrchr(runtime_lua_path, '/'); + if (!last_slash) { + return; + } + *last_slash = '\0'; + + char add_path[2300] = {0}; + snprintf(add_path, sizeof(add_path), "%s/lua/?.lua;%s/lua/?/init.lua", runtime_lua_path, runtime_lua_path); + + lua_getglobal(L, "package"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return; + } + + lua_getfield(L, -1, "path"); + const char *current = lua_tostring(L, -1); + if (!current) { + current = ""; + } + lua_pop(L, 1); + + char merged[4096] = {0}; + if (current[0] != '\0') { + snprintf(merged, sizeof(merged), "%s;%s", add_path, current); + } else { + snprintf(merged, sizeof(merged), "%s", add_path); + } + + lua_pushstring(L, merged); + lua_setfield(L, -2, "path"); + lua_pop(L, 1); +} + +bool lua_bridge_init(LuaBridge *bridge, Editor *editor) { + bridge->editor = editor; + bridge->runtime_init_path[0] = '\0'; + bridge->L = luaL_newstate(); + if (!bridge->L) { + return false; + } + + luaL_openlibs(bridge->L); + + lua_pushlightuserdata(bridge->L, editor); + lua_setfield(bridge->L, LUA_REGISTRYINDEX, EDITOR_REG_KEY); + + register_editor_api(bridge->L); + return true; +} + +void lua_bridge_free(LuaBridge *bridge) { + if (bridge->L) { + lua_close(bridge->L); + bridge->L = NULL; + } +} + +bool lua_bridge_load_runtime(LuaBridge *bridge, const char *init_script_path) { + configure_lua_package_path(bridge->L, init_script_path); + snprintf(bridge->runtime_init_path, sizeof(bridge->runtime_init_path), "%s", init_script_path ? init_script_path : ""); + + if (luaL_dofile(bridge->L, init_script_path) != LUA_OK) { + const char *err = lua_tostring(bridge->L, -1); + fprintf(stderr, "failed to load %s: %s\n", init_script_path, err ? err : "unknown"); + lua_pop(bridge->L, 1); + return false; + } + return true; +} + +bool lua_bridge_load_optional_init(LuaBridge *bridge, const char *script_path) { + if (!script_path || script_path[0] == '\0') { + return false; + } + + lua_getglobal(bridge->L, "dofile"); + if (!lua_isfunction(bridge->L, -1)) { + lua_pop(bridge->L, 1); + return false; + } + + lua_pushstring(bridge->L, script_path); + if (lua_pcall(bridge->L, 1, 0, 0) != LUA_OK) { + fprintf(stderr, "optional init load error (%s): %s\n", script_path, lua_tostring(bridge->L, -1)); + lua_pop(bridge->L, 1); + return false; + } + + return true; +} + +static bool call_global_with_optional_string(lua_State *L, const char *name, const char *arg) { + lua_getglobal(L, name); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 1); + return false; + } + + if (arg && arg[0] != '\0') { + lua_pushstring(L, arg); + } else { + lua_pushnil(L); + } + + if (lua_pcall(L, 1, 1, 0) != LUA_OK) { + fprintf(stderr, "lua error in %s: %s\n", name, lua_tostring(L, -1)); + lua_pop(L, 1); + return false; + } + + bool ok = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); + return ok; +} + +bool lua_bridge_call_open_file(LuaBridge *bridge, const char *path) { + return call_global_with_optional_string(bridge->L, "on_open_file", path); +} + +bool lua_bridge_call_text(LuaBridge *bridge, const char *text) { + return call_global_with_string(bridge->L, "on_text", text ? text : ""); +} + +bool lua_bridge_call_key(LuaBridge *bridge, const char *key_name) { + return call_global_with_string(bridge->L, "on_key", key_name); +} + +bool lua_bridge_call_tick(LuaBridge *bridge) { + return call_global_noargs(bridge->L, "on_tick"); +} diff --git a/src/core/lua_bridge.h b/src/core/lua_bridge.h new file mode 100644 index 0000000..9ff9123 --- /dev/null +++ b/src/core/lua_bridge.h @@ -0,0 +1,26 @@ +#ifndef LEMACS_LUA_BRIDGE_H +#define LEMACS_LUA_BRIDGE_H + +#include + +#include + +#include "core/editor.h" + +typedef struct { + lua_State *L; + Editor *editor; + char runtime_init_path[1024]; +} LuaBridge; + +bool lua_bridge_init(LuaBridge *bridge, Editor *editor); +void lua_bridge_free(LuaBridge *bridge); + +bool lua_bridge_load_runtime(LuaBridge *bridge, const char *init_script_path); +bool lua_bridge_load_optional_init(LuaBridge *bridge, const char *script_path); +bool lua_bridge_call_key(LuaBridge *bridge, const char *key_name); +bool lua_bridge_call_tick(LuaBridge *bridge); +bool lua_bridge_call_open_file(LuaBridge *bridge, const char *path); +bool lua_bridge_call_text(LuaBridge *bridge, const char *text); + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..fd7590c --- /dev/null +++ b/src/main.c @@ -0,0 +1,429 @@ +#include + +#include + +#include +#include +#include +#include +#include + +#include "core/buffer.h" +#include "core/editor.h" +#include "core/lua_bridge.h" + +#ifndef LEMACS_RUNTIME_INIT +#define LEMACS_RUNTIME_INIT "runtime/init.lua" +#endif + +static const int WINDOW_WIDTH = 1100; +static const int WINDOW_HEIGHT = 760; +static const int DEFAULT_FONT_SIZE = 20; + +typedef struct { + Font text; + bool custom; + int text_size; + float cell_width; +} LoadedFonts; + +static Color color_from_editor(EditorColor c) { + return (Color){c.r, c.g, c.b, c.a}; +} + +static bool resolve_font(const char *font_query, char *out_path, size_t out_size, int *out_size_px) { + if (!FcInit()) { + return false; + } + + FcPattern *pattern = FcNameParse((const FcChar8 *)font_query); + if (!pattern) { + return false; + } + + FcConfigSubstitute(NULL, pattern, FcMatchPattern); + FcDefaultSubstitute(pattern); + + FcResult result; + FcPattern *match = FcFontMatch(NULL, pattern, &result); + FcPatternDestroy(pattern); + if (!match) { + return false; + } + + FcChar8 *file = NULL; + if (FcPatternGetString(match, FC_FILE, 0, &file) != FcResultMatch || !file) { + FcPatternDestroy(match); + return false; + } + + snprintf(out_path, out_size, "%s", (const char *)file); + + double pixel_size = 0.0; + if (FcPatternGetDouble(match, FC_PIXEL_SIZE, 0, &pixel_size) == FcResultMatch && pixel_size > 0.0) { + *out_size_px = (int)lround(pixel_size); + } else { + double point_size = 0.0; + if (FcPatternGetDouble(match, FC_SIZE, 0, &point_size) == FcResultMatch && point_size > 0.0) { + *out_size_px = (int)lround(point_size); + } + } + + FcPatternDestroy(match); + return true; +} + +static bool load_single_font(const char *path, int size, Font *out_font) { + if (!path || path[0] == '\0' || size <= 0) { + return false; + } + + Font loaded = LoadFontEx(path, size, NULL, 0); + if (loaded.texture.id == 0) { + return false; + } + + GenTextureMipmaps(&loaded.texture); + SetTextureFilter(loaded.texture, TEXTURE_FILTER_TRILINEAR); + *out_font = loaded; + return true; +} + +static LoadedFonts load_fonts_from_query(const char *font_query) { + LoadedFonts fonts = {0}; + fonts.text = GetFontDefault(); + fonts.custom = false; + fonts.text_size = DEFAULT_FONT_SIZE; + fonts.cell_width = (float)DEFAULT_FONT_SIZE * 0.5f; + + if (!font_query || font_query[0] == '\0') { + font_query = "monospace:size=14"; + } + + int query_size = DEFAULT_FONT_SIZE; + char font_path[1024] = {0}; + if (resolve_font(font_query, font_path, sizeof(font_path), &query_size)) { + if (query_size < 10) { + query_size = 10; + } + + Font text_font = {0}; + if (load_single_font(font_path, query_size, &text_font)) { + fonts.text = text_font; + fonts.custom = true; + fonts.text_size = query_size; + fprintf(stderr, "lemacs: loaded font '%s' from %s\n", font_query, font_path); + } else { + fprintf(stderr, "lemacs: failed to load font file %s, using default\n", font_path); + } + } else { + fprintf(stderr, "lemacs: fontconfig could not resolve '%s', using default\n", font_query); + } + + Vector2 measure = MeasureTextEx(fonts.text, "M", (float)fonts.text_size, 0.0f); + fonts.cell_width = measure.x > 1.0f ? measure.x : (float)fonts.text_size * 0.5f; + return fonts; +} + +static void unload_loaded_fonts(LoadedFonts *fonts) { + if (fonts->custom) { + UnloadFont(fonts->text); + } + fonts->text = GetFontDefault(); + fonts->custom = false; + fonts->text_size = DEFAULT_FONT_SIZE; + fonts->cell_width = (float)DEFAULT_FONT_SIZE * 0.5f; +} + +static void reload_font_if_needed(Editor *editor, LoadedFonts *fonts) { + if (!editor_consume_font_dirty(editor)) { + return; + } + + unload_loaded_fonts(fonts); + *fonts = load_fonts_from_query(editor_font_query(editor)); +} + +static const char *key_name_from_input(int key, bool ctrl, bool shift) { + if (ctrl) { + if (key == KEY_Q) return "C-q"; + if (key == KEY_S) return "C-s"; + if (key == KEY_O) return "C-o"; + if (key == KEY_H) return "C-h"; + if (key == KEY_T) return "C-t"; + if (key == KEY_A) return "C-a"; + if (key == KEY_E) return "C-e"; + if (key == KEY_F) return "C-f"; + if (key == KEY_B) return "C-b"; + return NULL; + } + + if (shift) { + if (key == KEY_FOUR) return "$"; + if (key == KEY_SIX) return "^"; + if (key == KEY_SEMICOLON) return ":"; + } + + switch (key) { + case KEY_H: return "h"; + case KEY_J: return "j"; + case KEY_K: return "k"; + case KEY_L: return "l"; + case KEY_I: return "i"; + case KEY_A: return "a"; + case KEY_O: return "o"; + case KEY_D: return "d"; + case KEY_F: return "f"; + case KEY_BACKSLASH: return "\\"; + case KEY_SPACE: return "space"; + case KEY_W: return "w"; + case KEY_B: return "b"; + case KEY_LEFT: return "left"; + case KEY_RIGHT: return "right"; + case KEY_UP: return "up"; + case KEY_DOWN: return "down"; + case KEY_BACKSPACE: return "backspace"; + case KEY_DELETE: return "delete"; + case KEY_ENTER: return "enter"; + case KEY_TAB: return "tab"; + case KEY_ESCAPE: return "escape"; + default: return NULL; + } +} + +static void draw_cursor_shape(int x, int y, int cell_width, int font_size, Color cursor_color, int shape, int thickness) { + if (shape == LEMACS_CURSOR_BLOCK) { + DrawRectangle(x, y, cell_width > 1 ? cell_width : 1, font_size + 2, cursor_color); + return; + } + + if (shape == LEMACS_CURSOR_UNDERLINE) { + DrawRectangle(x, y + font_size + 1, cell_width > 1 ? cell_width : 1, thickness, cursor_color); + return; + } + + DrawRectangle(x, y, thickness, font_size + 2, cursor_color); +} + +static void draw_buffer_in_view(Buffer *buffer, size_t point, int scroll_line, Rectangle rect, Font font, int font_size, float cell_width, Color fg, bool show_cursor, Color cursor_color, int cursor_shape, int cursor_thickness) { + int line_height = font_size + 4; + int max_lines = (int)(rect.height / (float)line_height); + if (max_lines <= 0) { + return; + } + + BeginScissorMode((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height); + + size_t len = buffer_length(buffer); + size_t line_start = 0; + int current_line = 0; + while (line_start < len && current_line < scroll_line) { + if (buffer_char_at(buffer, line_start) == '\n') { + current_line++; + } + line_start++; + } + + for (int draw_line = 0; draw_line < max_lines && line_start <= len; ++draw_line) { + int col = 0; + size_t i = line_start; + while (i < len) { + char ch = buffer_char_at(buffer, i); + if (ch == '\n') { + i++; + break; + } + + char text[2] = {ch, '\0'}; + int x = (int)rect.x + (int)((float)col * cell_width); + int y = (int)rect.y + draw_line * line_height; + DrawTextEx(font, text, (Vector2){(float)x, (float)y}, (float)font_size, 0.0f, fg); + col++; + i++; + } + + if (i == line_start) { + break; + } + line_start = i; + } + + if (show_cursor) { + int cursor_line = 0; + int cursor_col = 0; + buffer_index_to_line_col(buffer, point, &cursor_line, &cursor_col); + int draw_line = cursor_line - scroll_line; + if (draw_line >= 0 && draw_line < max_lines) { + int x = (int)rect.x + (int)((float)cursor_col * cell_width); + int y = (int)rect.y + draw_line * line_height; + draw_cursor_shape(x, y, (int)cell_width, font_size, cursor_color, cursor_shape, cursor_thickness); + } + } + + EndScissorMode(); +} + +static Buffer *resolve_view_buffer(Editor *editor, const EditorView *view) { + if (view->source_type == 0) { + return editor_current_buffer(editor); + } + + if (view->source_type == 1) { + return editor_aux_buffer_get(editor, view->source_index); + } + + return NULL; +} + +static void ensure_primary_cursor_visible(Editor *editor, const EditorView *main_view, int font_size) { + if (!main_view || main_view->source_type != 0) { + return; + } + + int line_height = font_size + 4; + int max_lines = main_view->height / line_height; + if (max_lines <= 0) { + return; + } + + int line = 0; + int col = 0; + Buffer *buffer = editor_current_buffer(editor); + if (!buffer) { + return; + } + + buffer_index_to_line_col(buffer, buffer_point(buffer), &line, &col); + (void)col; + + if (line < editor->scroll_line) { + editor->scroll_line = line; + } else if (line >= editor->scroll_line + max_lines) { + editor->scroll_line = line - max_lines + 1; + } + + if (editor->scroll_line < 0) { + editor->scroll_line = 0; + } +} + +int main(int argc, char **argv) { + SetConfigFlags(FLAG_WINDOW_RESIZABLE | FLAG_VSYNC_HINT); + InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "lemacs"); + SetExitKey(KEY_NULL); + SetTargetFPS(60); + + Editor editor; + if (!editor_init(&editor)) { + fprintf(stderr, "failed to initialize editor\n"); + CloseWindow(); + return 1; + } + + LuaBridge bridge; + if (!lua_bridge_init(&bridge, &editor)) { + fprintf(stderr, "failed to initialize lua\n"); + editor_free(&editor); + CloseWindow(); + return 1; + } + + if (!lua_bridge_load_runtime(&bridge, LEMACS_RUNTIME_INIT)) { + lua_bridge_free(&bridge); + editor_free(&editor); + CloseWindow(); + return 1; + } + + lua_bridge_load_optional_init(&bridge, "init.lua"); + + LoadedFonts fonts = load_fonts_from_query(editor_font_query(&editor)); + + if (argc > 1) { + lua_bridge_call_open_file(&bridge, argv[1]); + } + + while (!WindowShouldClose() && !editor.should_quit) { + reload_font_if_needed(&editor, &fonts); + + bool ctrl = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL); + bool shift = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); + + int key = GetKeyPressed(); + while (key != 0) { + const char *name = key_name_from_input(key, ctrl, shift); + if (name) { + lua_bridge_call_key(&bridge, name); + } + key = GetKeyPressed(); + } + + if (!ctrl) { + int ch = GetCharPressed(); + while (ch > 0) { + if (ch >= 32 && ch != 127) { + char utf8[5] = {0}; + if (ch < 0x80) { + utf8[0] = (char)ch; + } else { + utf8[0] = '?'; + } + lua_bridge_call_text(&bridge, utf8); + } + ch = GetCharPressed(); + } + } + + lua_bridge_call_tick(&bridge); + + const EditorView *main_view = NULL; + for (int i = 0; i < editor.view_count; ++i) { + if (editor.views[i].source_type == 0 && editor.views[i].show_cursor) { + main_view = &editor.views[i]; + break; + } + } + ensure_primary_cursor_visible(&editor, main_view, fonts.text_size); + + BeginDrawing(); + Color bg_top = color_from_editor(editor.bg_top); + Color bg_bottom = color_from_editor(editor.bg_bottom); + DrawRectangleGradientV(0, 0, GetScreenWidth(), GetScreenHeight(), bg_top, bg_bottom); + + for (int i = 0; i < editor.view_count; ++i) { + EditorView *view = &editor.views[i]; + Buffer *buffer = resolve_view_buffer(&editor, view); + if (!buffer) { + continue; + } + + Rectangle rect = {(float)view->x, (float)view->y, (float)view->width, (float)view->height}; + DrawRectangle(view->x, view->y, view->width, view->height, color_from_editor(view->bg)); + + size_t point = view->source_type == 0 ? buffer_point(buffer) : 0; + int scroll_line = view->source_type == 0 ? editor.scroll_line : 0; + draw_buffer_in_view( + buffer, + point, + scroll_line, + rect, + fonts.text, + fonts.text_size, + fonts.cell_width, + color_from_editor(view->fg), + view->show_cursor && view->source_type == 0, + color_from_editor(editor.cursor), + editor_cursor_shape(&editor), + editor_cursor_thickness(&editor) + ); + } + + EndDrawing(); + } + + lua_bridge_free(&bridge); + editor_free(&editor); + unload_loaded_fonts(&fonts); + CloseWindow(); + return 0; +} diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..87dfb18 --- /dev/null +++ b/test.txt @@ -0,0 +1,5 @@ +# Sad machine +hello? is any body there? quiting. + + + -- cgit v1.3.1