From c146f860ccdc4330844c82e6ae65f0dac0e4f37e Mon Sep 17 00:00:00 2001 From: cinder <> Date: Fri, 10 May 2024 22:44:55 -0700 Subject: [PATCH] rework everything to use stdin/stdout with color --- build.lua | 20 +++- src/bin/lua.lua | 55 +++++++++++ src/bin/multitty.lua | 176 ++++++++++++++++++++++++++++++++++ src/kernel/02_io.lua | 29 +++++- src/kernel/04_environment.lua | 16 +++- src/lib/term.lua | 89 +++++++++++++++++ src/livedisk/init.lua | 28 +----- 7 files changed, 382 insertions(+), 31 deletions(-) create mode 100644 src/bin/lua.lua create mode 100644 src/bin/multitty.lua create mode 100644 src/lib/term.lua diff --git a/build.lua b/build.lua index af4623f..8019e8e 100644 --- a/build.lua +++ b/build.lua @@ -190,18 +190,32 @@ local function buildKernel(dst) end fs.makeDirectory(outdir) + +--boot eeprom exportFile('boot.lua') +--[=====[ core: a minimal system to build off of ]=====]-- fs.makeDirectory(outdir .. 'core/') buildKernel('core/init.lua') - +-- core bin/ fs.makeDirectory(outdir .. 'core/bin') -exportFile('bin/tty.lua', 'core/') +exportFile('bin/lua.lua', 'core/') +exportFile('bin/multitty.lua', 'core/') +-- core lib/ +fs.makeDirectory(outdir .. 'core/lib') +exportFile('lib/term.lua', 'core/') +--[=====[ livedisk: a self-contained bootable system for an introduction to kitn ]=====]-- fs.makeDirectory(outdir .. 'livedisk/') assert(fs.copy(outdir .. 'core/init.lua', outdir .. 'livedisk/init.lua')) +-- livedisk bin/ fs.makeDirectory(outdir .. 'livedisk/bin/') -assert(fs.copy(outdir .. 'core/bin/tty.lua', outdir .. 'livedisk/bin/tty.lua')) +assert(fs.copy(outdir .. 'core/bin/lua.lua', outdir .. 'livedisk/bin/lua.lua')) +assert(fs.copy(outdir .. 'core/bin/multitty.lua', outdir .. 'livedisk/bin/multitty.lua')) +-- livedisk lib/ +fs.makeDirectory(outdir .. 'livedisk/lib/') +assert(fs.copy(outdir .. 'core/lib/term.lua', outdir .. 'livedisk/lib/term.lua')) +-- livedisk etc/ fs.makeDirectory(outdir .. 'livedisk/etc/') exportFile('livedisk/init.lua', 'livedisk/etc/init.lua') diff --git a/src/bin/lua.lua b/src/bin/lua.lua new file mode 100644 index 0000000..0276c68 --- /dev/null +++ b/src/bin/lua.lua @@ -0,0 +1,55 @@ +local ok, err = xpcall(function(...) +local tty = ... +io.stdin, io.stdout = tty, tty +local term = dofile('/lib/term.lua').wrap(io) --TODO require() + +term:write(_VERSION, ' // ', _OSVERSION, '\n') + +local env = setmetatable({}, {__index = _ENV}) --TODO pcall require() on __index + +::repl:: +local line, err = term:prompt({ fg = 0x00ff00, 'lua> ' }) +if not line then + if err == nil then --eof + return + elseif err == 'interrupted' then + term:write('\n') + goto repl + else + error(err) + end +end + +local f, err = load('return ' .. line, '=expr', 't', env) +if not f then f, err = load(line, '=repl', 't', env) end +if not f then + term:error(tostring(err), '\n') + goto repl +end + +local results = table.pack(pcall(f)) +if not results[1] then + term:error('error: ', tostring(results[2]), '\n') + goto repl +end + +for i = 2, results.n do + local val = results[i] + local t = type(val) + if i > 2 then term:write('\t') end + if t == 'nil' or t == 'boolean' or t == 'number' then + term:write({ fg = 0xffff00, tostring(val) }) + elseif t == 'string' then + term:write({ fg = 0x9292ff, string.format('%q', val) }) + else + term:write({ fg = 0xff80ff, tostring(val) }) + end +end + +if results.n > 1 then term:write('\n') end + +goto repl +end, function(e) return debug.traceback(tostring(e)) end, ...) +if not ok then error(err, 0) end + +-- vi: set ts=2: diff --git a/src/bin/multitty.lua b/src/bin/multitty.lua new file mode 100644 index 0000000..cbf8fe6 --- /dev/null +++ b/src/bin/multitty.lua @@ -0,0 +1,176 @@ +--provide a tty on each available screen, running the given program +local progArgs = table.pack(...) +checkArg(1, progArgs[1], 'string') --program path + + +--identify the highest-tier gpu available +local gpu, vmem --use virtual memory as a heuristic for tier +for addr in component.list('gpu', true) do + local vmemNew = component.invoke(addr, 'totalMemory') + if vmemNew > (vmem or -1) then + gpu, vmem = addr, vmemNew + end +end +gpu = assert(gpu, 'please install a GPU or APU to continue') + +--track most recently assigned values to skip redundant bind/setBackground/setForeground calls +local curScreen, curBg, curFg +local function bind(screen) + if screen ~= curScreen then + component.invoke(gpu, 'bind', screen) + curScreen = screen + end +end +local function setBackground(color) + if color ~= curBg then + component.invoke(gpu, 'setBackground', color) + curBg = color + end +end +local function setForeground(color) + if color ~= curFg then + component.invoke(gpu, 'setForeground', color) + curFg = color + end +end +local function draw(cx, text) + bind(cx.screen) + setBackground(cx.bg) + setForeground(cx.fg) + return component.invoke(gpu, 'set', cx.x, cx.y, text) +end + +local iomt = {} +iomt.__index = iomt + +local function isAttachedKeyboard(cx, addr) + for i, kb in ipairs(cx.keyboards) do + if kb == addr then + return i + end + end +end + +--tail-recursively pullSignal to construct the line of input read from a terminal +local function readSignal(cx, input, sig, ...) + if sig == 'key_down' then + local kb, ch, code = ... + if isAttachedKeyboard(cx, kb) then + if code == 28 then --enter + cx.x, cx.y = 1, cx.y + 1 + return input .. '\n' + elseif code == 14 then --backspace + if input ~= '' then + cx.x = cx.x - 1 + draw(cx, ' ') + input = input:sub(1, -2) + end + elseif ch ~= 0 then + local char = string.char(ch) + draw(cx, char) + cx.x = cx.x + 1 + input = input .. char + end + end + elseif sig == 'component_added' then + local addr, ty = ... + if ty == 'keyboard' then + cx.keyboards = component.invoke(screen, 'getKeyboards') + end + elseif sig == 'component_removed' then + local addr, ty = ... + if addr == cx.screen then + return nil, 'disconnected' + end + local idx = ty == 'keyboard' and isAttachedKeyboard(cx, addr) + if idx then + table.remove(cx.keyboards, idx) + end + end + + return readSignal(cx, input, computer.pullSignal()) +end + +-- file handle methods +function iomt:read(n) + --technically this should return at maximum `n` bytes, but that would involve a redundant second buffer + --we happen to know the implementation works fine returning more than `n` + + return readSignal(self, '', computer.pullSignal()) + --[[ + local sig = table.pack(computer.pullSignal()) + for i = 1, sig.n do + sig[i] = string.format(type(sig[i]) == 'string' and '%q' or '%s', sig[i]) + end + return table.concat(sig, ', ', 1, sig.n) .. '\n' + ]] +end + +function iomt:write(data) + bind(self.screen) setBackground(self.bg) setForeground(self.fg) + local idx = 1 + ::line:: + local lf = data:find('\n', idx, true) + component.invoke(gpu, 'set', self.x, self.y, data:sub(idx, lf and lf - 1)) + + if lf then + self.x, self.y = 1, self.y + 1 + idx = lf + 1 + goto line + end + + self.x = self.x + #data - idx + 1 + return true +end + +--no seek, noop close +function iomt:close() return true end + +-- tty methods +function iomt:mark() + return { bg = self.bg, fg = self.fg } +end +function iomt:reset(mark) + self.bg, self.fg = mark.bg, mark.fg +end +function iomt:setBg(color) self.bg = color end +function iomt:setFg(color) self.fg = color end + + +local function createTerm(screen, prog, ...) + checkArg(1, screen, 'string') + checkArg(2, prog, 'string') + local fh = setmetatable({ screen = screen, keyboards = component.invoke(screen, 'getKeyboards'), x = 1, y = 1, fg = 0xffffff, bg = 0x000000 }, iomt) + local stdio = assert(kitn.wrapFile('r+', fh)) + stdio.tty = fh + + bind(screen) + setForeground(0xffffff) + setBackground(0x000000) + local w, h = component.invoke(gpu, 'maxResolution') + component.invoke(gpu, 'setResolution', w, h) + component.invoke(gpu, 'fill', 1, 1, w, h, ' ') + + local process = assert(kitn.createProcess(prog, stdio, ...)) --TODO set io via createProcess args + return process, stdio +end + + + +--for each existing screen, spawn prog +for screen in component.list('screen') do + createTerm(screen, table.unpack(progArgs, 1, progArgs.n)) +end + +--when new screens are added, spawn prog on those too +local function tick(sig, addr, ty) + if sig == 'component_added' and ty == 'screen' then + createTerm(addr, table.unpack(progArgs, 1, progArgs.n)) + end + + return tick(computer.pullSignal()) +end + +return tick(computer.pullSignal()) + +-- vi: set ts=2: diff --git a/src/kernel/02_io.lua b/src/kernel/02_io.lua index e8b4c35..6d2bdf7 100644 --- a/src/kernel/02_io.lua +++ b/src/kernel/02_io.lua @@ -1,4 +1,4 @@ -local openNull, openPath +local openNull, openPath, wrapFile do local filedata = setmetatable({}, { __mode = 'k' }) --weak-keyed mapping of file object to data/inner (fh of wrapFile) local filemt = { __metatable = '' } @@ -13,7 +13,7 @@ do end end - local function wrapFile(mode, fh) + function wrapFile(mode, fh) local rwa, b, plus, b2 = match(mode or 'r', '^([rwa])(b?)(%+?)(b?)$') --match all of x, xb, x+, xb+, x+b if not rwa or (b ~= '' and b2 ~= '') then return nil, 'bad mode' end @@ -123,6 +123,31 @@ do fh._file_rbuf = '' return cat end + elseif n == 'l' then + local lf1, lf2 = fh._file_rbuf:find('\n') + if lf1 then + local line = fh._file_rbuf:sub(1, lf1 - 1) + fh._file_rbuf = fh._file_rbuf:sub(lf2 + 1) + return line + end + + ::read:: + + local parts = { fh._file_rbuf } + local chunk, err = fh:read(512) --TODO tune default capacity to e.g. free memory + if not chunk then + fh._file_rbuf = concat(parts) + return nil, err + end + + parts[#parts+1] = chunk + + lf1, lf2 = chunk:find('\n') + if not lf1 then goto read end + + fh._file_rbuf = chunk:sub(lf2 + 1) + parts[#parts] = chunk:sub(1, lf1 - 1) + return concat(parts) else error('TODO read1 patterns') end diff --git a/src/kernel/04_environment.lua b/src/kernel/04_environment.lua index 75f472d..c814fd7 100644 --- a/src/kernel/04_environment.lua +++ b/src/kernel/04_environment.lua @@ -9,7 +9,7 @@ given : ]] -envBase._OSVERSION = 'kitn 0.2.0-beta.3' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver" +envBase._OSVERSION = 'kitn 0.2.0-beta.4' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver" envBase.kitn = {} envBase._G, envBase.load = nil @@ -184,6 +184,20 @@ function fillEnv(proc, uenv) end return openPath(mode, address, path) end + + local function wrap_read(fh, ...) return fh.stream:read(...) end + local function wrap_write(fh, ...) return fh.stream:write(...) end + local function wrap_seek(fh, ...) return fh.stream:seek(...) end + local function wrap_close(fh, ...) return fh.stream:close(...) end + + --TODO expose this as a require()able library like + function ukitn.wrapFile(mode, stream) + checkArg(1, mode, 'string') + checkArg(2, stream, 'table') + --in the kernel, we can take a shortcut and add _file_* attributes directly to the caller's (our own) fh. we don't + --want to do that to arbitrary userspace values though, so we add another layer of indirection. + return wrapFile(mode, { stream = stream, read = wrap_read, write = wrap_write, seek = wrap_seek, close = wrap_close }) + end end -- vi: set ts=2: diff --git a/src/lib/term.lua b/src/lib/term.lua new file mode 100644 index 0000000..254ea3c --- /dev/null +++ b/src/lib/term.lua @@ -0,0 +1,89 @@ +local mt = {} +mt.__index = mt + +--wrap the current process's stdio streams into a tty that supports styled output. +local function wrap(io_) + checkArg(1, io_, 'table', 'nil') + io_ = io_ or io + + --cache the streams here so that `local tty = wrap(io); io.output('/some/file'); tty.prompt()` works as expected + local stdin, stdout, stderr = io_.stdin, io_.stdout, io_.stderr + if stdout.tty then stdout:setvbuf('no') end + if stderr.tty then stderr:setvbuf('no') end + + return setmetatable({ stdin = stdin, stdout = stdout, stderr = stderr }, mt) +end + +local function writeStyled(tty, stream, arg) + if type(arg) == 'table' then + local token = tty:mark() + if arg.bg then tty:setBg(arg.bg) end + if arg.fg then tty:setFg(arg.fg) end + for i = 1, arg.n or #arg do writeStyled(tty, stream, arg[i]) end + stream:flush() --ensure + tty:reset(token) + else + stream:write(arg) + end +end + +local function writeUnstyled(stream, arg) + if type(arg) == 'table' then + for i = 1, arg.n or #arg do + writeUnstyled(stream, arg[i]) + end + else + stream:write(arg) + end +end + +--[[write strings to stdout with optional styling. + +tables are assumed to be styled groups, ie sequences of strings/styled groups with optional extra properties: + .fg = 0xrrggbb sets foreground color for all contained text, unless overridden by a styled group inside + .bg = 0xrrggbb sets background color for all contained text, unless overridden by a styled group inside +these properties are applied while rendering the styled group, then reverted to the prior values before rendering +any following values in this or future write() calls. + +if stdout is not a tty, all styling is ignored. +]] +function mt:write(...) + local args = table.pack(...) + local caps = self.stdout.tty + if caps then + writeStyled(caps, self.stdout, args) + else + writeUnstyled(self.stdout, args) + end + return self +end + +--write strings to stderr with optional styling. +--see `mt:write` for how styling is interpreted. +function mt:error(...) + --[[ + local args = table.pack(...) + local caps = self.stderr.tty + if caps then + writeStyled(caps, self.stderr, args) + else + writeUnstyled(self.stderr, args) + end + return self + ]] + return self:write({ fg = 0xff0000, ... }) +end + +--prompt the user for a line of input. +--if stdout isn't a tty, equivalent to `io.stdin:read('l')`. +function mt:prompt(...) + if self.stdin.tty and self.stdout.tty then + self:write(...) + self.stdout:flush() + end + return self.stdin:read('l') +end + +return { wrap = wrap } + +-- vi: set ts=2: diff --git a/src/livedisk/init.lua b/src/livedisk/init.lua index 0ea28b1..f043a75 100644 --- a/src/livedisk/init.lua +++ b/src/livedisk/init.lua @@ -1,28 +1,6 @@ --/etc/init.lua for the kitn livedisk and installer ---find highest-tier gpu available -local bestGpu, bestMem -for addr in component.list('gpu') do - local mem = component.invoke(addr, 'totalMemory') - if mem > (bestMem or 0) then - bestGpu, bestMem = addr, mem - end -end +--spawn a lua repl on each available screen +return dofile('/bin/multitty.lua', '/bin/lua.lua') -assert(bestGpu, 'please install a gpu or apu to continue') - -for screen in component.list('screen') do - local proc, err = kitn.createProcess('/bin/tty.lua', bestGpu, screen) - if not proc then error('spawn for ' .. screen:sub(1, 4) .. ' failed: ' .. err) end -end - -local function tick(signal, addr, ty) - if signal == 'component_added' and ty == 'screen' then - local proc, err = kitn.createProcess('/bin/tty.lua', bestGpu, addr) - if not proc then error('spawn for ' .. screen:sub(1, 4) .. ' failed: ' .. err) end - end - - return tick(computer.pullSignal()) -end - -return tick(computer.pullSignal()) +-- vi: set ts=2: