init
This commit is contained in:
8
apps/adventure.lua
Normal file
8
apps/adventure.lua
Normal file
@@ -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()
|
||||||
120
apps/launcher.lua
Normal file
120
apps/launcher.lua
Normal file
@@ -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()
|
||||||
7
apps/shell.lua
Normal file
7
apps/shell.lua
Normal file
@@ -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()
|
||||||
7
apps/worm.lua
Normal file
7
apps/worm.lua
Normal file
@@ -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()
|
||||||
649
libs/compat.lua
Normal file
649
libs/compat.lua
Normal file
@@ -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
|
||||||
20
libs/keybinds.lua
Normal file
20
libs/keybinds.lua
Normal file
@@ -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
|
||||||
35
libs/threading.lua
Normal file
35
libs/threading.lua
Normal file
@@ -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
|
||||||
335
libs/window.lua
Normal file
335
libs/window.lua
Normal file
@@ -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
|
||||||
98
modules/interactions.lua
Normal file
98
modules/interactions.lua
Normal file
@@ -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
|
||||||
34
modules/keybinds.lua
Normal file
34
modules/keybinds.lua
Normal file
@@ -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
|
||||||
2
modules/launcher.lua
Normal file
2
modules/launcher.lua
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
local kb = require("libs.keybinds")
|
||||||
|
kb.register(kb.keybind():addKey(keys.leftAlt):addKey(keys.a), loadfile("apps/launcher.lua"))
|
||||||
97
startup.lua
Normal file
97
startup.lua
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user