rework everything to use stdin/stdout with color

main
cinder 6 months ago
parent 2fad8db849
commit c146f860cc

@ -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')

@ -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:

@ -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:

@ -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 = '<file>' }
@ -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

@ -9,7 +9,7 @@ given <https://ocdoc.cil.li/tutorial:custom_oses#what_s_available>:
]]
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 <https://ocdoc.cil.li/api:buffer>
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:

@ -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:

@ -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:

Loading…
Cancel
Save