commit fb77ed8c767c98d0ebadc139c6842c71373615cb Author: Caden Ream Date: Fri Oct 17 08:54:58 2025 -0700 init diff --git a/apps/adventure.lua b/apps/adventure.lua new file mode 100644 index 0000000..874c532 --- /dev/null +++ b/apps/adventure.lua @@ -0,0 +1,8 @@ +local compat = require("libs.compat") +local window = require("libs.window") +local x, y = term.getSize() +local win = window.create("Adventure", x / 1.4, y / 1.4, x / 2 - ((x / 1.4) / 2), y / 2 - ((y / 1.5) / 2)) +sleep() +compat.runFile("/rom/programs/fun/adventure.lua", win) +sleep(5) +win.close() diff --git a/apps/launcher.lua b/apps/launcher.lua new file mode 100644 index 0000000..e4b08a6 --- /dev/null +++ b/apps/launcher.lua @@ -0,0 +1,120 @@ +local window = require("libs.window") +local threading = require("libs.threading") +local x, y = term.getSize() +local win = window.create("Shell", x / 3, y / 1.4, x / 2 - ((x / 3) / 2), y / 2 - ((y / 1.5) / 2)) +win.decorations = false +win.alwaysOnTop = true +local apps = {} +-- tiny alphabetical boost based on the first A–Z letter in the name +local function alpha_boost(name, scale) + scale = scale or 0.001 -- tune this: 0.001 ⇒ max boost ≈ 0.026 for 'a' + name = string.lower(name or "") + -- find first alphabetic char + local i = name:find("%a") + if not i then return 0 end + local ch = name:sub(i, i) + local byte = ch:byte() + -- map a..z -> 1..26; non-letters -> 0 boost + if byte < 97 or byte > 122 then return 0 end + local pos = byte - 96 -- 1 for 'a', 26 for 'z' + return (27 - pos) * scale -- 'a' highest, 'z' lowest +end + +-- literal (plain) substring counter with optional overlap +local function count_sub(s, sub, overlap) + if sub == "" then return 0 end + local count, i = 0, 0 + while true do + local j = string.find(s, sub, i + 1, true) + if not j then break end + count = count + 1 + i = overlap and j or (j + #sub - 1) + end + return count +end + +local function score(app, search) + local name = string.lower(app.name or "") + local q = string.lower(search or "") + + local s = count_sub(name, q, true) * 2 + if app.tags then + for _, tag in ipairs(app.tags) do + s = s + count_sub(string.lower(tag), q, true) + end + end + + -- Optional extra nudge if the name starts with the query (nice UX) + if q ~= "" and name:sub(1, #q) == q then + s = s + 0.5 + end + + -- Alphabetical tiebreaker (very small) + s = s + alpha_boost(app.name) + + return s +end +for _, f in ipairs(fs.list("/.apps")) do + local file = fs.open(fs.combine("/.apps", f), "r") + if file then + local v = textutils.unserialise(file.readAll()) + if v and v.name and v.file then + apps[#apps + 1] = v + end + file.close() + end +end + +local function clamp(v, lo, hi) return (v < lo) and lo or ((v > hi) and hi or v) end + +sleep() + +local search = "" +local scroll = 1 +local runnning = true +function win.char(ch) + search = search .. ch +end + +function win.key(key) + if key == keys.backspace then + search = search:sub(1, #search - 1) + elseif key == keys.up then + scroll = clamp(scroll - 1, 1, #apps) + elseif key == keys.down then + scroll = clamp(scroll + 1, 1, #apps) + elseif key == keys.enter then + local app = apps[scroll] + threading.addFromFile(app.file) + runnning = false + elseif key == keys.leftCtrl then + runnning = false + end +end + +while runnning do + table.sort(apps, function(a, b) return score(a, search) > score(b, search) end) + win.setBackgroundColor(colors.gray) + win.clear() + for i, app in ipairs(apps) do + if i == scroll then + win.setBackgroundColor(colors.white) + win.setTextColor(colors.black) + else + win.setBackgroundColor(colors.gray) + win.setTextColor(colors.white) + end + win.setCursorPos(1, i) + win.clearLine() + win.write(app.name) + end + local _, y = win.getSize() + win.setCursorPos(1, y) + win.setTextColor(colors.white) + win.setBackgroundColor(colors.black) + win.clearLine() + win.write(search) + sleep() +end + +win.close() diff --git a/apps/shell.lua b/apps/shell.lua new file mode 100644 index 0000000..4e64366 --- /dev/null +++ b/apps/shell.lua @@ -0,0 +1,7 @@ +local compat = require("libs.compat") +local window = require("libs.window") +local x, y = term.getSize() +local win = window.create("Shell", x / 1.4, y / 1.4, x / 2 - ((x / 1.4) / 2), y / 2 - ((y / 1.5) / 2)) +sleep() +compat.runFile("/rom/programs/shell.lua", win) +win.close() diff --git a/apps/worm.lua b/apps/worm.lua new file mode 100644 index 0000000..0dcb704 --- /dev/null +++ b/apps/worm.lua @@ -0,0 +1,7 @@ +local compat = require("libs.compat") +local window = require("libs.window") +local x, y = term.getSize() +local win = window.create("Worm", x / 1.4, y / 1.4, x / 2 - ((x / 1.4) / 2), y / 2 - ((y / 1.5) / 2)) +sleep() +compat.runFile("/rom/programs/fun/worm.lua", win) +win.close() diff --git a/libs/compat.lua b/libs/compat.lua new file mode 100644 index 0000000..54b45fd --- /dev/null +++ b/libs/compat.lua @@ -0,0 +1,649 @@ +local lib = {} + +local expect +do + local h = fs.open("rom/modules/main/cc/expect.lua", "r") + local f, err = loadstring(h.readAll(), "@/rom/modules/main/cc/expect.lua") + h.close() + + if not f then error(err) end + expect = f().expect +end + +function lib.setupENV(win) + local expect = dofile("rom/modules/main/cc/expect.lua").expect + + local native = term.native and term.native() or term + local redirectTarget = native + + local function wrap(_sFunction) + return function(...) + return redirectTarget[_sFunction](...) + end + end + + local term = {} + + --- Redirects terminal output to a monitor, a [`window`], or any other custom + -- terminal object. Once the redirect is performed, any calls to a "term" + -- function - or to a function that makes use of a term function, as [`print`] - + -- will instead operate with the new terminal object. + -- + -- A "terminal object" is simply a table that contains functions with the same + -- names - and general features - as those found in the term table. For example, + -- a wrapped monitor is suitable. + -- + -- The redirect can be undone by pointing back to the previous terminal object + -- (which this function returns whenever you switch). + -- + -- @tparam Redirect target The terminal redirect the [`term`] API will draw to. + -- @treturn Redirect The previous redirect object, as returned by + -- [`term.current`]. + -- @since 1.31 + -- @usage + -- Redirect to a monitor on the right of the computer. + -- + -- term.redirect(peripheral.wrap("right")) + term.redirect = function(target) + expect(1, target, "table") + if target == term or target == _G.term then + error("term is not a recommended redirect target, try term.current() instead", 2) + end + for k, v in pairs(native) do + if type(k) == "string" and type(v) == "function" then + if type(target[k]) ~= "function" then + target[k] = function() + error("Redirect object is missing method " .. k .. ".", 2) + end + end + end + end + local oldRedirectTarget = redirectTarget + redirectTarget = target + return oldRedirectTarget + end + + --- Returns the current terminal object of the computer. + -- + -- @treturn Redirect The current terminal redirect + -- @since 1.6 + -- @usage + -- Create a new [`window`] which draws to the current redirect target. + -- + -- window.create(term.current(), 1, 1, 10, 10) + term.current = function() + return redirectTarget + end + + --- Get the native terminal object of the current computer. + -- + -- It is recommended you do not use this function unless you absolutely have + -- to. In a multitasked environment, [`term.native`] will _not_ be the current + -- terminal object, and so drawing may interfere with other programs. + -- + -- @treturn Redirect The native terminal redirect. + -- @since 1.6 + term.native = function() + return native + end + + -- Some methods shouldn't go through redirects, so we move them to the main + -- term API. + for _, method in ipairs { "nativePaletteColor", "nativePaletteColour" } do + term[method] = native[method] + native[method] = nil + end + + for k, v in pairs(native) do + if type(k) == "string" and type(v) == "function" and rawget(term, k) == nil then + term[k] = wrap(k) + end + end + term.redirect(win) + local function read(_sReplaceChar, _tHistory, _fnComplete, _sDefault) + expect(1, _sReplaceChar, "string", "nil") + expect(2, _tHistory, "table", "nil") + expect(3, _fnComplete, "function", "nil") + expect(4, _sDefault, "string", "nil") + + term.setCursorBlink(true) + + local sLine + if type(_sDefault) == "string" then + sLine = _sDefault + else + sLine = "" + end + local nHistoryPos + local nPos, nScroll = #sLine, 0 + if _sReplaceChar then + _sReplaceChar = string.sub(_sReplaceChar, 1, 1) + end + + local tCompletions + local nCompletion + local function recomplete() + if _fnComplete and nPos == #sLine then + tCompletions = _fnComplete(sLine) + if tCompletions and #tCompletions > 0 then + nCompletion = 1 + else + nCompletion = nil + end + else + tCompletions = nil + nCompletion = nil + end + end + + local function uncomplete() + tCompletions = nil + nCompletion = nil + end + + local w = term.getSize() + local sx = term.getCursorPos() + + local function redraw(_bClear) + local cursor_pos = nPos - nScroll + if sx + cursor_pos >= w then + -- We've moved beyond the RHS, ensure we're on the edge. + nScroll = sx + nPos - w + elseif cursor_pos < 0 then + -- We've moved beyond the LHS, ensure we're on the edge. + nScroll = nPos + end + + local _, cy = term.getCursorPos() + term.setCursorPos(sx, cy) + local sReplace = _bClear and " " or _sReplaceChar + if sReplace then + term.write(string.rep(sReplace, math.max(#sLine - nScroll, 0))) + else + term.write(string.sub(sLine, nScroll + 1)) + end + + if nCompletion then + local sCompletion = tCompletions[nCompletion] + local oldText, oldBg + if not _bClear then + oldText = term.getTextColor() + oldBg = term.getBackgroundColor() + term.setTextColor(colors.white) + term.setBackgroundColor(colors.gray) + end + if sReplace then + term.write(string.rep(sReplace, #sCompletion)) + else + term.write(sCompletion) + end + if not _bClear then + term.setTextColor(oldText) + term.setBackgroundColor(oldBg) + end + end + + term.setCursorPos(sx + nPos - nScroll, cy) + end + + local function clear() + redraw(true) + end + + recomplete() + redraw() + + local function acceptCompletion() + if nCompletion then + -- Clear + clear() + + -- Find the common prefix of all the other suggestions which start with the same letter as the current one + local sCompletion = tCompletions[nCompletion] + sLine = sLine .. sCompletion + nPos = #sLine + + -- Redraw + recomplete() + redraw() + end + end + while true do + local sEvent, param, param1, param2 = os.pullEvent() + if sEvent == "char" then + -- Typed key + clear() + sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1) + nPos = nPos + 1 + recomplete() + redraw() + elseif sEvent == "paste" then + -- Pasted text + clear() + sLine = string.sub(sLine, 1, nPos) .. param .. string.sub(sLine, nPos + 1) + nPos = nPos + #param + recomplete() + redraw() + elseif sEvent == "key" then + if param == keys.enter or param == keys.numPadEnter then + -- Enter/Numpad Enter + if nCompletion then + clear() + uncomplete() + redraw() + end + break + elseif param == keys.left then + -- Left + if nPos > 0 then + clear() + nPos = nPos - 1 + recomplete() + redraw() + end + elseif param == keys.right then + -- Right + if nPos < #sLine then + -- Move right + clear() + nPos = nPos + 1 + recomplete() + redraw() + else + -- Accept autocomplete + acceptCompletion() + end + elseif param == keys.up or param == keys.down then + -- Up or down + if nCompletion then + -- Cycle completions + clear() + if param == keys.up then + nCompletion = nCompletion - 1 + if nCompletion < 1 then + nCompletion = #tCompletions + end + elseif param == keys.down then + nCompletion = nCompletion + 1 + if nCompletion > #tCompletions then + nCompletion = 1 + end + end + redraw() + elseif _tHistory then + -- Cycle history + clear() + if param == keys.up then + -- Up + if nHistoryPos == nil then + if #_tHistory > 0 then + nHistoryPos = #_tHistory + end + elseif nHistoryPos > 1 then + nHistoryPos = nHistoryPos - 1 + end + else + -- Down + if nHistoryPos == #_tHistory then + nHistoryPos = nil + elseif nHistoryPos ~= nil then + nHistoryPos = nHistoryPos + 1 + end + end + if nHistoryPos then + sLine = _tHistory[nHistoryPos] + nPos, nScroll = #sLine, 0 + else + sLine = "" + nPos, nScroll = 0, 0 + end + uncomplete() + redraw() + end + elseif param == keys.backspace then + -- Backspace + if nPos > 0 then + clear() + sLine = string.sub(sLine, 1, nPos - 1) .. string.sub(sLine, nPos + 1) + nPos = nPos - 1 + if nScroll > 0 then nScroll = nScroll - 1 end + recomplete() + redraw() + end + elseif param == keys.home then + -- Home + if nPos > 0 then + clear() + nPos = 0 + recomplete() + redraw() + end + elseif param == keys.delete then + -- Delete + if nPos < #sLine then + clear() + sLine = string.sub(sLine, 1, nPos) .. string.sub(sLine, nPos + 2) + recomplete() + redraw() + end + elseif param == keys["end"] then + -- End + if nPos < #sLine then + clear() + nPos = #sLine + recomplete() + redraw() + end + elseif param == keys.tab then + -- Tab (accept autocomplete) + acceptCompletion() + end + elseif sEvent == "mouse_click" or sEvent == "mouse_drag" and param == 1 then + local _, cy = term.getCursorPos() + if param1 >= sx and param1 <= w and param2 == cy then + -- Ensure we don't scroll beyond the current line + nPos = math.min(math.max(nScroll + param1 - sx, 0), #sLine) + redraw() + end + elseif sEvent == "term_resize" then + -- Terminal resized + w = term.getSize() + redraw() + end + end + + local _, cy = term.getCursorPos() + term.setCursorBlink(false) + term.setCursorPos(w + 1, cy) + print() + + return sLine + end + + + local function write(sText) + expect(1, sText, "string", "number") + + local w, h = term.getSize() + local x, y = term.getCursorPos() + + local nLinesPrinted = 0 + local function newLine() + if y + 1 <= h then + term.setCursorPos(1, y + 1) + else + term.setCursorPos(1, h) + term.scroll(1) + end + x, y = term.getCursorPos() + nLinesPrinted = nLinesPrinted + 1 + end + + -- Print the line with proper word wrapping + sText = tostring(sText) + while #sText > 0 do + local whitespace = string.match(sText, "^[ \t]+") + if whitespace then + -- Print whitespace + term.write(whitespace) + x, y = term.getCursorPos() + sText = string.sub(sText, #whitespace + 1) + end + + local newline = string.match(sText, "^\n") + if newline then + -- Print newlines + newLine() + sText = string.sub(sText, 2) + end + + local text = string.match(sText, "^[^ \t\n]+") + if text then + sText = string.sub(sText, #text + 1) + if #text > w then + -- Print a multiline word + while #text > 0 do + if x > w then + newLine() + end + term.write(text) + text = string.sub(text, w - x + 2) + x, y = term.getCursorPos() + end + else + -- Print a word normally + if x + #text - 1 > w then + newLine() + end + term.write(text) + x, y = term.getCursorPos() + end + end + end + + return nLinesPrinted + end + function print(...) + local nLinesPrinted = 0 + local nLimit = select("#", ...) + for n = 1, nLimit do + local s = tostring(select(n, ...)) + if n < nLimit then + s = s .. "\t" + end + nLinesPrinted = nLinesPrinted + write(s) + end + nLinesPrinted = nLinesPrinted + write("\n") + return nLinesPrinted + end + + local function printError(...) + local oldColour + if term.isColour() then + oldColour = term.getTextColour() + term.setTextColour(colors.red) + end + print(...) + if term.isColour() then + term.setTextColour(oldColour) + end + end + local tAPIsLoading = {} + + local bAPIError = false + + local env = setmetatable({ + term = term, + write = write, + read = read, + shell = shell, + print = print, + printError = printError + }, { __index = _G }) + + local function loadAPI(_sPath) + expect(1, _sPath, "string") + local sName = fs.getName(_sPath) + if sName:sub(-4) == ".lua" then + sName = sName:sub(1, -5) + end + if sName == "term" then + return true + end + if tAPIsLoading[sName] == true then + printError("API " .. sName .. " is already being loaded") + return false + end + tAPIsLoading[sName] = true + + local tEnv = {} + setmetatable(tEnv, { __index = env }) + local fnAPI, err = loadfile(_sPath, nil, tEnv) + if fnAPI then + local ok, err = pcall(fnAPI) + if not ok then + tAPIsLoading[sName] = nil + return error("Failed to load API " .. sName .. " due to " .. err, 1) + end + else + tAPIsLoading[sName] = nil + return error("Failed to load API " .. sName .. " due to " .. err, 1) + end + + local tAPI = {} + for k, v in pairs(tEnv) do + if k ~= "_ENV" then + tAPI[k] = v + end + end + env[sName] = tAPI + tAPIsLoading[sName] = nil + return true + end + + local function load_apis(dir) + if not fs.isDir(dir) then return end + + for _, file in ipairs(fs.list(dir)) do + if file:sub(1, 1) ~= "." then + local path = fs.combine(dir, file) + if not fs.isDir(path) then + if not loadAPI(path) then + bAPIError = true + end + end + end + end + end + + load_apis("rom/apis") + if http then load_apis("rom/apis/http") end + if turtle then load_apis("rom/apis/turtle") end + if pocket then load_apis("rom/apis/pocket") end + env._ENV = env + env._G = env + + return env +end + +local function generate_id(length) + local id = "" + for _ = 1, length do + id = id .. string.char(math.random(97, 122)) + end + return id +end + +local function runInRuntime(func, win) + expect(1, func, "function") + expect(2, win, "table") + local co = coroutine.create(func) + local winid = generate_id(40) + local filter = nil + local event_data = { n = 0 } + local run = true + function win.clicked(xc, yc, button) + os.queueEvent("mouse_click_" .. winid, button, xc, yc, winid) + end + + function win.released(xc, yc, button) + os.queueEvent("mouse_up_" .. winid, button, xc, yc, winid) + end + + function win.dragged(xc, yc, button) + os.queueEvent("mouse_drag_" .. winid, button, xc, yc, winid) + end + + function win.scrolled(dir, xc, yc) + os.queueEvent("mouse_scroll_" .. winid, dir, xc, yc, winid) + end + + function win.char(ch) + os.queueEvent("char_" .. winid, ch, winid) + end + + function win.key(key, is_held) + os.queueEvent("key_" .. winid, key, is_held, winid) + end + + function win.key_up(key) + os.queueEvent("key_up_" .. winid, key, winid) + end + + function win.closeRequested() + run = false + end + + local function escape_lua_pattern(s) + -- magic chars: ( ) . % + - * ? [ ^ $ ] + return (s:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")) + end + + local start = true + while run and coroutine.status(co) ~= "dead" do + if event_data.n > 0 or start then + local ok, msg = coroutine.resume(co, table.unpack(event_data, 1, event_data.n)) + start = false + if ok then + filter = msg + else + error(msg) + end + end + local data = table.pack(os.pullEvent()) + data[1] = data[1]:gsub(escape_lua_pattern "_" .. winid, "") + if data[1] == filter or filter == nil or data[1] == "terminated" then + if data[1] == "char" then + if data[#data] == winid then + data.n = data.n - 1 + event_data = data + end + elseif data[1] == "key" then + if data[#data] == winid then + data.n = data.n - 1 + event_data = data + end + elseif data[1] == "mouse_click" then + if data[#data] == winid then + data.n = data.n - 1 + event_data = data + end + elseif data[1] == "mouse_drag" then + if data[#data] == winid then + data.n = data.n - 1 + event_data = data + end + elseif data[1] == "key_up" then + if data[#data] == winid then + data.n = data.n - 1 + event_data = data + end + elseif data[1] == "mouse_up" then + if data[#data] == winid then + data.n = data.n - 1 + event_data = data + end + elseif data[1] == "mouse_scroll" then + if data[#data] == winid then + data.n = data.n - 1 + event_data = data + end + else + event_data = data + end + else + event_data = { n = 0 } + end + end +end + +function lib.runFunc(func, win) + runInRuntime(setfenv(func, lib.setupENV(win)), win) +end + +function lib.runFile(file, win) + local func = loadfile(file) + runInRuntime(setfenv(func, lib.setupENV(win)), win) +end + +return lib diff --git a/libs/keybinds.lua b/libs/keybinds.lua new file mode 100644 index 0000000..96b6a66 --- /dev/null +++ b/libs/keybinds.lua @@ -0,0 +1,20 @@ +local keybinds = {} +function keybinds.keybind() + local keybind = { keys = {} } + function keybind.addKey(self, key) + assert(type(key) == "number", "expected number.. got " .. type(key)) + keybind.keys[#keybind.keys + 1] = key + return self + end + + return keybind +end + +function keybinds.register(keybind, func) + assert(type(keybind) == "table", "expected table for arg #1.. got " .. type(keybind)) + assert(type(func) == "function", "expected function for arg #2.. got " .. type(func)) + assert(keybind.keys ~= nil, "arg #1 is not a keybind") + _G.keybinds[#_G.keybinds + 1] = { kb = keybind, func = func, pressed = false } +end + +return keybinds diff --git a/libs/threading.lua b/libs/threading.lua new file mode 100644 index 0000000..5a2c685 --- /dev/null +++ b/libs/threading.lua @@ -0,0 +1,35 @@ +local lib = {} + +local function add(t, v) + for i = 1, #t + 1 do + if t[i] == nil then + t[i] = v + return i + end + end +end + +function lib.addThread(func, env) + env = setmetatable(env or {}, { __index = _ENV }) + env._ENV = env + env._G = env + local id = add(_G.threads, { co = coroutine.create(setfenv(func, env)) }) + os.queueEvent("thread", id) + return id +end + +function lib.addFromFile(file, env) + local func = loadfile(file) + env = setmetatable(env or {}, { __index = _ENV }) + local id = add(_G.threads, { + co = coroutine.create(setfenv(func, env)) + }) + os.queueEvent("thread", id) + return id +end + +function lib.rmThread(id) + _G.threads[id] = nil +end + +return lib diff --git a/libs/window.lua b/libs/window.lua new file mode 100644 index 0000000..06f729f --- /dev/null +++ b/libs/window.lua @@ -0,0 +1,335 @@ +local lib = {} + +local function add(t, v) + for i = 1, #t + 1 do + if t[i] == nil then + t[i] = v + return + end + end +end + +function lib.reorder() + local temp = {} + local top = {} + local bottom = {} + for _, i in pairs(_G.windows) do + if i.alwaysOnTop then + top[#top + 1] = i + elseif i.alwaysBelow then + temp[#temp + 1] = i + else + bottom[#bottom + 1] = i + end + end + _G.windows = bottom + for _, i in pairs(temp) do + _G.windows[#_G.windows + 1] = i + end + for _, i in pairs(top) do + _G.windows[#_G.windows + 1] = i + end +end + +-- hex <-> color bit lookups (Lua 5.1 safe) +local HEX = "0123456789abcdef" +local hex_to_color, color_to_hex = {}, {} +for i = 1, 16 do + local c = HEX:sub(i, i) + local v = bit32 and bit32.lshift(1, i - 1) or 2 ^ (i - 1) + hex_to_color[c] = v + color_to_hex[v] = c +end + +local function normalize_color(c) + if color_to_hex[c] then return c end + if type(c) ~= "number" or c < 1 or c > 0xffff then + error("Colour out of range", 2) + end + -- match base's parse_color: coerce mask -> highest set bit (power-of-two) + return 2 ^ math.floor(math.log(c, 2)) +end + +local function clamp(v, lo, hi) return (v < lo) and lo or ((v > hi) and hi or v) end + +function lib.create(name, w, h, x, y) + w = math.floor(w + 0.5) + h = math.floor(h + 0.5) + x = math.floor(x + 0.5) + y = math.floor(y + 0.5) + -- x,y are metadata for you; renderer can use them + local t = { + name = name, + w = w, + h = h, + x = x or 1, + y = y or 2, + + -- column-major buffer: buffer[x][y] = {char, tc, bc} + buffer = {}, + cursorX = 1, + cursorY = 1, + textColor = colors.white, + bgColor = colors.black, + cursorBlink = false, + decorations = true, + alwaysOnTop = false, + alwaysBelow = false, + closing = false, + _palette = {}, -- optional local palette store + } + + local function init_col(xi) + if not t.buffer[xi] then + t.buffer[xi] = {} + for yy = 1, t.h do + t.buffer[xi][yy] = { char = " ", tc = t.textColor, bc = t.bgColor } + end + end + end + + function t.clear() + for xi = 1, t.w do + init_col(xi) + for yy = 1, t.h do + local cell = t.buffer[xi][yy] + cell.char, cell.tc, cell.bc = " ", t.textColor, t.bgColor + end + end + t.cursorX, t.cursorY = 1, 1 + end + + function t.current() return t end + + function t.redirect() end + + function t.clearLine() + local y0 = t.cursorY + if y0 < 1 or y0 > t.h then return end + for xi = 1, t.w do + init_col(xi) + local cell = t.buffer[xi][y0] + cell.char, cell.tc, cell.bc = " ", t.textColor, t.bgColor + end + -- base keeps cursorX unchanged + end + + function t.getSize() return t.w, t.h end + + function t.setCursorPos(x0, y0) + -- do NOT clamp; let writers clip like base window + t.cursorX = math.floor(x0) + t.cursorY = math.floor(y0) + end + + function t.getCursorPos() return t.cursorX, t.cursorY end + + function t.setCursorBlink(b) t.cursorBlink = not not b end + + function t.getCursorBlink() return t.cursorBlink end + + function t.setTextColor(c) t.textColor = normalize_color(c) end + + t.setTextColour = t.setTextColor + + function t.getTextColor() return t.textColor end + + t.getTextColour = t.getTextColor + + function t.setBackgroundColor(c) t.bgColor = normalize_color(c) end + + t.setBackgroundColour = t.setBackgroundColor + + function t.getBackgroundColor() return t.bgColor end + + t.getBackgroundColour = t.getBackgroundColor + + function t.isColor() + return term.native().isColor() + end + + t.isColour = t.isColor + + -- Palette passthrough: store locally; renderer can apply + function t.setPaletteColor(col, r, g, b) + if type(r) == "number" and g and b then + t._palette[col] = { r, g, b } + elseif type(r) == "number" then + -- assume 0xRRGGBB integer (no bitops) + local v = r + local R8 = math.floor(v / 65536) % 256 + local G8 = math.floor(v / 256) % 256 + local B8 = v % 256 + t._palette[col] = { R8 / 255, G8 / 255, B8 / 255 } + end + end + + t.setPaletteColour = t.setPaletteColor + + function t.getPaletteColor(col) + local p = t._palette[col] + if p then return p[1], p[2], p[3] end + -- base always returns numbers; fall back to native + return term.native().getPaletteColour(col) + end + + t.getPaletteColour = t.getPaletteColor + + -- write/blit: mutate buffer with clipping; let cursor run past width + function t.write(str) + str = tostring(str) + local y0 = t.cursorY + for i = 1, #str do + local x = t.cursorX + i - 1 + if x >= 1 and x <= t.w and y0 >= 1 and y0 <= t.h then + init_col(x) + local cell = t.buffer[x][y0] + cell.char = str:sub(i, i) + cell.tc, cell.bc = t.textColor, t.bgColor + end + end + t.cursorX = t.cursorX + #str + end + + function t.blit(text, textColors, bgColors) + if type(text) ~= "string" then error("bad argument #1 (expected string)", 2) end + if textColors and type(textColors) ~= "string" then error("bad argument #2 (expected string)", 2) end + if bgColors and type(bgColors) ~= "string" then error("bad argument #3 (expected string)", 2) end + if textColors and #textColors ~= #text then error("Arguments must be the same length", 2) end + if bgColors and #bgColors ~= #text then error("Arguments must be the same length", 2) end + + textColors = textColors and textColors:lower() or nil + bgColors = bgColors and bgColors:lower() or nil + + local y0 = t.cursorY + local n = #text + for i = 1, n do + local x = t.cursorX + i - 1 + if x >= 1 and x <= t.w and y0 >= 1 and y0 <= t.h then + init_col(x) + local ch = text:sub(i, i) + local tch = textColors and textColors:sub(i, i) or nil + local bch = bgColors and bgColors:sub(i, i) or nil + local tc = tch and hex_to_color[tch] or t.textColor + local bc = bch and hex_to_color[bch] or t.bgColor + + local cell = t.buffer[x][y0] or { char = " ", tc = t.textColor, bc = t.bgColor } + cell.char, cell.tc, cell.bc = ch, tc, bc + end + end + t.cursorX = t.cursorX + n + end + + -- scroll: supports positive (up) and negative (down) + function t.scroll(n) + n = math.floor(n or 1) + if n == 0 then return end + + local absn = math.abs(n) + if absn >= t.h then + -- clear all rows to new empty lines with current colors + for xi = 1, t.w do + init_col(xi) + for yy = 1, t.h do + local cell = t.buffer[xi][yy] or { char = " ", tc = t.textColor, bc = t.bgColor } + cell.char, cell.tc, cell.bc = " ", t.textColor, t.bgColor + end + end + return + end + + if n > 0 then + -- move content up + for xi = 1, t.w do + init_col(xi) + for yy = 1, t.h - n do + local dst, src = t.buffer[xi][yy], t.buffer[xi][yy + n] + dst.char, dst.tc, dst.bc = src.char, src.tc, src.bc + end + for yy = t.h - n + 1, t.h do + local cell = t.buffer[xi][yy] or { char = " ", tc = t.textColor, bc = t.bgColor } + cell.char, cell.tc, cell.bc = " ", t.textColor, t.bgColor + end + end + else + -- n < 0 : move content down + local k = -n + for xi = 1, t.w do + init_col(xi) + for yy = t.h, k + 1, -1 do + local dst, src = t.buffer[xi][yy], t.buffer[xi][yy - k] + dst.char, dst.tc, dst.bc = src.char, src.tc, src.bc + end + for yy = 1, k do + local cell = t.buffer[xi][yy] + cell.char, cell.tc, cell.bc = " ", t.textColor, t.bgColor + end + end + end + end + + -- input hooks (no-op; your UI can override) + function t.clicked(xc, yc, button) end + + function t.released(xc, yc, button) end + + function t.dragged(xc, yc, button) end + + function t.scrolled(dir, xc, yc) end + + function t.char(ch) end + + function t.key(key, is_held) end + + function t.key_up(key) end + + function t.closeRequested() t.closing = true end + + function t.close() + t.closing = true + end + + -- ---- Compatibility shims with base window (no rendering inside) ---- + + function t.getPosition() return t.x, t.y end + + function t.reposition(nx, ny, nw, nh, _new_parent) + nw = math.floor(nw + 0.5) + nh = math.floor(nh + 0.5) + nx = math.floor(nx + 0.5) + ny = math.floor(ny + 0.5) + if type(nx) ~= "number" or type(ny) ~= "number" then error("bad position", 2) end + t.x, t.y = nx, ny + + if nw and nh then + if type(nw) ~= "number" or type(nh) ~= "number" then error("bad size", 2) end + local newbuf = {} + for xi = 1, nw do + newbuf[xi] = {} + for yy = 1, nh do + local from = + (t.buffer[xi] and t.buffer[xi][yy]) and t.buffer[xi][yy] + or { char = " ", tc = t.textColor, bc = t.bgColor } + newbuf[xi][yy] = { char = from.char, tc = from.tc, bc = from.bc } + end + end + t.buffer, t.w, t.h = newbuf, nw, nh + end + end + + function t.redraw() end -- renderer handles this elsewhere + + function t.setVisible(_) end + + function t.isVisible() return true end + + function t.restoreCursor() end + + -- initialize + t.clear() + add(_G.windows, t) + lib.reorder() + return t +end + +return lib diff --git a/modules/interactions.lua b/modules/interactions.lua new file mode 100644 index 0000000..5c61a18 --- /dev/null +++ b/modules/interactions.lua @@ -0,0 +1,98 @@ +local threading = require("libs.threading") +local dragging = nil +local offsetX, offsetY = 0, 0 +local function bringtofront(indx) + local win = _G.windows[indx] + if win.alwaysOnTop or win.alwaysBelow then return end + _G.windows[indx] = nil + local temp = {} + local top = {} + local bottom = {} + for _, i in pairs(_G.windows) do + if i.alwaysOnTop then + top[#top + 1] = i + elseif i.alwaysBelow then + temp[#temp + 1] = i + else + bottom[#bottom + 1] = i + end + end + temp[#temp + 1] = win + _G.windows = bottom + for _, i in pairs(temp) do + _G.windows[#_G.windows + 1] = i + end + for _, i in pairs(top) do + _G.windows[#_G.windows + 1] = i + end +end +while true do + local data = { os.pullEvent() } + if data[1] == "mouse_click" then + for indx = #_G.windows, 1, -1 do + local win = _G.windows[indx] + if win.y - 1 == data[4] and win.x + 1 <= data[3] and win.x + win.w >= data[3] and data[2] == 1 and win.decorations then + dragging = win + offsetX = win.x - data[3] + offsetY = win.y - data[4] + bringtofront(indx) + break + elseif win.y - 1 == data[4] and win.x == data[3] and win.decorations then + threading.addThread(function() win.closeRequested() end) + bringtofront(indx) + break + elseif win.y <= data[4] and win.x <= data[3] and win.y + win.h > data[4] and win.x + win.w > data[3] then + threading.addThread(function() win.clicked(data[3] - win.x + 1, data[4] - win.y + 1, data[2]) end) + bringtofront(indx) + break + end + end + elseif data[1] == "mouse_drag" then + if data[2] == 1 and dragging then + dragging.x = data[3] + offsetX + dragging.y = data[4] + offsetY + else + for indx = #_G.windows, 1, -1 do + local win = _G.windows[indx] + if win.y <= data[4] and win.x <= data[3] and win.y + win.h > data[4] and win.x + win.w > data[3] then + threading.addThread(function() win.dragged(data[3] - win.x + 1, data[4] - win.y + 1, data[2]) end) + bringtofront(indx) + break + end + end + end + elseif data[1] == "mouse_up" then + if data[2] == 1 and dragging then + dragging = nil + else + for indx = #_G.windows, 1, -1 do + local win = _G.windows[indx] + if win.y <= data[4] and win.x <= data[3] and win.y + win.h > data[4] and win.x + win.w > data[3] then + threading.addThread(function() win.released(data[3] - win.x + 1, data[4] - win.y + 1, data[2]) end) + bringtofront(indx) + break + end + end + end + elseif data[1] == "mouse_scroll" then + for indx = #_G.windows, 1, -1 do + local win = _G.windows[indx] + if win.y <= data[4] and win.x <= data[3] and win.y + win.h > data[4] and win.x + win.w > data[3] then + threading.addThread(function() win.scrolled(data[2], data[3] - win.x + 1, data[4] - win.y + 1) end) + break + end + end + elseif data[1] == "key" then + if _G.windows[#_G.windows] then + threading.addThread(function() _G.windows[#_G.windows].key(data[2], data[3]) end) + end + elseif data[1] == "char" then + if _G.windows[#_G.windows] then + threading.addThread(function() _G.windows[#_G.windows].char(data[2]) end) + end + elseif data[1] == "key_up" then + if _G.windows[#_G.windows] then + threading.addThread(function() _G.windows[#_G.windows].key_up(data[2]) end) + end + end +end diff --git a/modules/keybinds.lua b/modules/keybinds.lua new file mode 100644 index 0000000..bd32ad6 --- /dev/null +++ b/modules/keybinds.lua @@ -0,0 +1,34 @@ +local keystate = {} +local threading = require("libs.threading") +local function isEqual(t1, t2) + if #t1 ~= #t2 then return false end + for k = 1, #t1 do + if t1[k] ~= t2[k] then return false end + end + return true +end +while true do + local proto, key, is_held = os.pullEvent() + if proto == "key" and not is_held then + keystate[#keystate + 1] = key + elseif proto == "key_up" then + for k, v in ipairs(keystate) do + if v == key then + keystate[k] = nil + end + end + local tempkeystate = {} + for _, v in pairs(keystate) do + tempkeystate[#tempkeystate + 1] = v + end + keystate = tempkeystate + end + for _, keybinding in ipairs(_G.keybinds) do + if isEqual(keybinding.kb.keys, keystate) and not keybinding.pressed then + threading.addThread(keybinding.func) + keybinding.pressed = true + elseif not isEqual(keybinding.kb.keys, keystate) then + keybinding.pressed = false + end + end +end diff --git a/modules/launcher.lua b/modules/launcher.lua new file mode 100644 index 0000000..5482122 --- /dev/null +++ b/modules/launcher.lua @@ -0,0 +1,2 @@ +local kb = require("libs.keybinds") +kb.register(kb.keybind():addKey(keys.leftAlt):addKey(keys.a), loadfile("apps/launcher.lua")) diff --git a/startup.lua b/startup.lua new file mode 100644 index 0000000..fa4025a --- /dev/null +++ b/startup.lua @@ -0,0 +1,97 @@ +--os.pullEvent = os.pullEventRaw +local window = require("libs.window") +local wrap = require("cc.strings").wrap +_G.threads = {} +_G.windows = {} +_G.keybinds = {} +local term = term.native() +local event = { n = 0 } +local function threads() + for id, thr in pairs(_G.threads) do + if thr then + if coroutine.status(thr.co) ~= "dead" then + if thr.filter == nil or thr.filter == event[1] or event[1] == "terminate" then + local ok, msg = coroutine.resume(thr.co, table.unpack(event, 1, event.n)) + if not ok then + msg = tostring(msg) + local wrapped_lines = wrap(msg, 23) + local win = window.create("Error", 25, #wrapped_lines + 2) + for y, i in ipairs(wrapped_lines) do + win.setCursorPos(2, y + 1) + win.setTextColor(colors.red) + win.write(i) + end + _G.threads[id] = nil + else + thr.filter = msg + end + end + else + _G.threads[id] = nil + end + end + end + event = { os.pullEventRaw() } +end + +local function windows() + term.setCursorBlink(false) + for id, win in ipairs(_G.windows) do + for cy = 1, win.h do + term.setCursorPos(win.x, win.y + cy - 1) + local line, fg, bg = "", "", "" + for cx = 1, win.w do + local cell = win.buffer[cx][cy] + line = line .. cell.char + fg = fg .. ("0123456789abcdef"):sub(math.log(cell.tc, 2) + 1, math.log(cell.tc, 2) + 1) + bg = bg .. ("0123456789abcdef"):sub(math.log(cell.bc, 2) + 1, math.log(cell.bc, 2) + 1) + end + term.blit(line, fg, bg) + end + if win.decorations then + term.setCursorPos(win.x, win.y - 1) + term.setTextColor(colors.white) + term.setBackgroundColor(colors.gray) + term.write("X " .. win.name .. string.rep(" ", win.w - #win.name - 2)) + end + term.setCursorPos(win.x + win.cursorX - 1, win.y + win.cursorY - 1) + term.setCursorBlink(win.cursorBlink) + if win.closing then + _G.windows[id] = nil + end + end + window.reorder() +end + +local function desktop() + local w, h = term.getSize() + term.setBackgroundColor(colors.lightGray) + term.clear() + term.setCursorPos(1, 1) + term.setTextColor(colors.white) + term.setBackgroundColor(colors.gray) + term.write(" Desktop ") +end +local threading = require("libs.threading") +local compat = require("libs.compat") +for _, i in ipairs(fs.list("/modules")) do + if not fs.isDir("/modules/" .. i) then + threading.addFromFile("/modules/" .. i) + end +end + +local function render() + while true do + desktop() + windows() + sleep(1 / 20) + end +end + +local function process() + while true do + threads() + end +end + +parallel.waitForAny(process, render) diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..1c8ca60 --- /dev/null +++ b/test.lua @@ -0,0 +1,4 @@ +while true do + local _, mb, x, y = os.pullEvent("mouse_scroll") + print(x, y) +end