├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── init.lua └── src ├── 3rd ├── README.md ├── filesystem.lua ├── full_filesystem.lua └── tty.lua ├── devfs.lua ├── eventhooks.lua ├── filesystem.lua ├── loadfile.lua ├── main.lua ├── printk.lua └── scheduler.lua /.gitignore: -------------------------------------------------------------------------------- 1 | boot/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "opencomputers-autocompletions"] 2 | path = opencomputers-autocompletions 3 | url = https://github.com/atirut-w/opencomputers-autocompletions 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @mkdir -p boot/ 3 | @cd src/ && luacomp main.lua -O ../boot/kernel.lua 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WIP 2 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | if computer.getArchitecture() ~= "Lua 5.3" then 2 | computer.setArchitecture("Lua 5.3") 3 | end 4 | 5 | component.proxy(component.list("gpu")()).set(1,1,"Loading kernel...") 6 | 7 | ---@type function 8 | local kernel 9 | do 10 | ---@type FilesystemProxy 11 | local fs = component.proxy(computer.getBootAddress()) 12 | local handle = fs.open("/boot/kernel.lua") 13 | 14 | if not handle then 15 | error("Kernel not found", 0) 16 | end 17 | 18 | local k_content = "" 19 | repeat 20 | local data = fs.read(handle, math.huge) 21 | k_content = k_content .. (data or "") 22 | until not data 23 | 24 | local err 25 | kernel, err = load(k_content, "=kernel", "t", _G) 26 | if not kernel then 27 | error(err, 0) 28 | end 29 | end 30 | 31 | local k_coroutine = coroutine.create(kernel) 32 | repeat 33 | local ok, err = coroutine.resume(k_coroutine) 34 | if not ok then 35 | error(debug.traceback(k_coroutine, err), 0) 36 | end 37 | until coroutine.status(k_coroutine) == "dead" 38 | -------------------------------------------------------------------------------- /src/3rd/README.md: -------------------------------------------------------------------------------- 1 | # Third party libraries 2 | 3 | + OpenOS libraries 4 | + `filesystem.lua` 5 | + `full_filesystem.lua` 6 | + Cynosure libraries 7 | + `tty.lua` 8 | -------------------------------------------------------------------------------- /src/3rd/filesystem.lua: -------------------------------------------------------------------------------- 1 | -- local component = require("component") 2 | -- local unicode = require("unicode") 3 | local unicode = string 4 | 5 | local filesystem = {} 6 | local mtab = {name="", children={}, links={}} 7 | local fstab = {} 8 | 9 | local function segments(path) 10 | local parts = {} 11 | for part in path:gmatch("[^\\/]+") do 12 | local current, up = part:find("^%.?%.$") 13 | if current then 14 | if up == 2 then 15 | table.remove(parts) 16 | end 17 | else 18 | table.insert(parts, part) 19 | end 20 | end 21 | return parts 22 | end 23 | 24 | local function findNode(path, create, resolve_links) 25 | checkArg(1, path, "string") 26 | local visited = {} 27 | local parts = segments(path) 28 | local ancestry = {} 29 | local node = mtab 30 | local index = 1 31 | while index <= #parts do 32 | local part = parts[index] 33 | ancestry[index] = node 34 | if not node.children[part] then 35 | local link_path = node.links[part] 36 | if link_path then 37 | if not resolve_links and #parts == index then break end 38 | 39 | if visited[path] then 40 | return nil, string.format("link cycle detected '%s'", path) 41 | end 42 | -- the previous parts need to be conserved in case of future ../.. link cuts 43 | visited[path] = index 44 | local pst_path = "/" .. table.concat(parts, "/", index + 1) 45 | local pre_path 46 | 47 | if link_path:match("^[^/]") then 48 | pre_path = table.concat(parts, "/", 1, index - 1) .. "/" 49 | local link_parts = segments(link_path) 50 | local join_parts = segments(pre_path .. link_path) 51 | local back = (index - 1 + #link_parts) - #join_parts 52 | index = index - back 53 | node = ancestry[index] 54 | else 55 | pre_path = "" 56 | index = 1 57 | node = mtab 58 | end 59 | 60 | path = pre_path .. link_path .. pst_path 61 | parts = segments(path) 62 | part = nil -- skip node movement 63 | elseif create then 64 | node.children[part] = {name=part, parent=node, children={}, links={}} 65 | else 66 | break 67 | end 68 | end 69 | if part then 70 | node = node.children[part] 71 | index = index + 1 72 | end 73 | end 74 | 75 | local vnode, vrest = node, #parts >= index and table.concat(parts, "/", index) 76 | local rest = vrest 77 | while node and not node.fs do 78 | rest = rest and filesystem.concat(node.name, rest) or node.name 79 | node = node.parent 80 | end 81 | return node, rest, vnode, vrest 82 | end 83 | 84 | ------------------------------------------------------------------------------- 85 | 86 | function filesystem.canonical(path) 87 | local result = table.concat(segments(path), "/") 88 | if unicode.sub(path, 1, 1) == "/" then 89 | return "/" .. result 90 | else 91 | return result 92 | end 93 | end 94 | 95 | function filesystem.concat(...) 96 | local set = table.pack(...) 97 | for index, value in ipairs(set) do 98 | checkArg(index, value, "string") 99 | end 100 | return filesystem.canonical(table.concat(set, "/")) 101 | end 102 | 103 | function filesystem.get(path) 104 | local node = findNode(path) 105 | if node.fs then 106 | local proxy = node.fs 107 | path = "" 108 | while node and node.parent do 109 | path = filesystem.concat(node.name, path) 110 | node = node.parent 111 | end 112 | path = filesystem.canonical(path) 113 | if path ~= "/" then 114 | path = "/" .. path 115 | end 116 | return proxy, path 117 | end 118 | return nil, "no such file system" 119 | end 120 | 121 | function filesystem.realPath(path) 122 | checkArg(1, path, "string") 123 | local node, rest = findNode(path, false, true) 124 | if not node then return nil, rest end 125 | local parts = {rest or nil} 126 | repeat 127 | table.insert(parts, 1, node.name) 128 | node = node.parent 129 | until not node 130 | return table.concat(parts, "/") 131 | end 132 | 133 | function filesystem.mount(fs, path) 134 | checkArg(1, fs, "string", "table") 135 | if type(fs) == "string" then 136 | fs = filesystem.proxy(fs) 137 | end 138 | assert(type(fs) == "table", "bad argument #1 (file system proxy or address expected)") 139 | checkArg(2, path, "string") 140 | 141 | local real 142 | if not mtab.fs then 143 | if path == "/" then 144 | real = path 145 | else 146 | return nil, "rootfs must be mounted first" 147 | end 148 | else 149 | local why 150 | real, why = filesystem.realPath(path) 151 | if not real then 152 | return nil, why 153 | end 154 | 155 | if filesystem.exists(real) and not filesystem.isDirectory(real) then 156 | return nil, "mount point is not a directory" 157 | end 158 | end 159 | 160 | local fsnode 161 | if fstab[real] then 162 | return nil, "another filesystem is already mounted here" 163 | end 164 | for _,node in pairs(fstab) do 165 | if node.fs.address == fs.address then 166 | fsnode = node 167 | break 168 | end 169 | end 170 | 171 | if not fsnode then 172 | fsnode = select(3, findNode(real, true)) 173 | -- allow filesystems to intercept their own nodes 174 | fs.fsnode = fsnode 175 | else 176 | local pwd = filesystem.path(real) 177 | local parent = select(3, findNode(pwd, true)) 178 | local name = filesystem.name(real) 179 | fsnode = setmetatable({name=name,parent=parent},{__index=fsnode}) 180 | parent.children[name] = fsnode 181 | end 182 | 183 | fsnode.fs = fs 184 | fstab[real] = fsnode 185 | 186 | return true 187 | end 188 | 189 | function filesystem.path(path) 190 | local parts = segments(path) 191 | local result = table.concat(parts, "/", 1, #parts - 1) .. "/" 192 | if unicode.sub(path, 1, 1) == "/" and unicode.sub(result, 1, 1) ~= "/" then 193 | return "/" .. result 194 | else 195 | return result 196 | end 197 | end 198 | 199 | function filesystem.name(path) 200 | checkArg(1, path, "string") 201 | local parts = segments(path) 202 | return parts[#parts] 203 | end 204 | 205 | function filesystem.proxy(filter, options) 206 | checkArg(1, filter, "string") 207 | if not component.list("filesystem")[filter] or next(options or {}) then 208 | -- if not, load fs full library, it has a smarter proxy that also supports options 209 | return filesystem.internal.proxy(filter, options) 210 | end 211 | return component.proxy(filter) -- it might be a perfect match 212 | end 213 | 214 | function filesystem.exists(path) 215 | if not filesystem.realPath(filesystem.path(path)) then 216 | return false 217 | end 218 | local node, rest, vnode, vrest = findNode(path) 219 | if not vrest or vnode.links[vrest] then -- virtual directory or symbolic link 220 | return true 221 | elseif node and node.fs then 222 | return node.fs.exists(rest) 223 | end 224 | return false 225 | end 226 | 227 | function filesystem.isDirectory(path) 228 | local real, reason = filesystem.realPath(path) 229 | if not real then return nil, reason end 230 | local node, rest, vnode, vrest = findNode(real) 231 | if not vnode.fs and not vrest then 232 | return true -- virtual directory (mount point) 233 | end 234 | if node.fs then 235 | return not rest or node.fs.isDirectory(rest) 236 | end 237 | return false 238 | end 239 | 240 | function filesystem.list(path) 241 | local node, rest, vnode, vrest = findNode(path, false, true) 242 | local result = {} 243 | if node then 244 | result = node.fs and node.fs.list(rest or "") or {} 245 | -- `if not vrest` indicates that vnode reached the end of path 246 | -- in other words, vnode[children, links] represent path 247 | if not vrest then 248 | for k,n in pairs(vnode.children) do 249 | if not n.fs or fstab[filesystem.concat(path, k)] then 250 | table.insert(result, k .. "/") 251 | end 252 | end 253 | for k in pairs(vnode.links) do 254 | table.insert(result, k) 255 | end 256 | end 257 | end 258 | local set = {} 259 | for _,name in ipairs(result) do 260 | set[filesystem.canonical(name)] = name 261 | end 262 | return function() 263 | local key, value = next(set) 264 | set[key or false] = nil 265 | return value 266 | end 267 | end 268 | 269 | function filesystem.open(path, mode) 270 | checkArg(1, path, "string") 271 | mode = tostring(mode or "r") 272 | checkArg(2, mode, "string") 273 | 274 | assert(({r=true, rb=true, w=true, wb=true, a=true, ab=true})[mode], 275 | "bad argument #2 (r[b], w[b] or a[b] expected, got " .. mode .. ")") 276 | 277 | local node, rest = findNode(path, false, true) 278 | if not node then 279 | return nil, rest 280 | end 281 | if not node.fs or not rest or (({r=true,rb=true})[mode] and not node.fs.exists(rest)) then 282 | return nil, "file not found" 283 | end 284 | 285 | local handle, reason = node.fs.open(rest, mode) 286 | if not handle then 287 | return nil, reason 288 | end 289 | 290 | return setmetatable({ 291 | fs = node.fs, 292 | handle = handle, 293 | }, {__index = function(tbl, key) 294 | if not tbl.fs[key] then return end 295 | if not tbl.handle then 296 | return nil, "file is closed" 297 | end 298 | return function(self, ...) 299 | local h = self.handle 300 | if key == "close" then 301 | self.handle = nil 302 | end 303 | return self.fs[key](h, ...) 304 | end 305 | end}) 306 | end 307 | 308 | filesystem.findNode = findNode 309 | filesystem.segments = segments 310 | filesystem.fstab = fstab 311 | 312 | ------------------------------------------------------------------------------- 313 | 314 | --#include "3rd/full_filesystem.lua" 315 | return filesystem 316 | -------------------------------------------------------------------------------- /src/3rd/full_filesystem.lua: -------------------------------------------------------------------------------- 1 | -- local filesystem = require("filesystem") 2 | -- local component = require("component") 3 | -- local shell = require("shell") 4 | 5 | function filesystem.makeDirectory(path) 6 | if filesystem.exists(path) then 7 | return nil, "file or directory with that name already exists" 8 | end 9 | local node, rest = filesystem.findNode(path) 10 | if node.fs and rest then 11 | local success, reason = node.fs.makeDirectory(rest) 12 | if not success and not reason and node.fs.isReadOnly() then 13 | reason = "filesystem is readonly" 14 | end 15 | return success, reason 16 | end 17 | if node.fs then 18 | return nil, "virtual directory with that name already exists" 19 | end 20 | return nil, "cannot create a directory in a virtual directory" 21 | end 22 | 23 | function filesystem.lastModified(path) 24 | local node, rest, vnode, vrest = filesystem.findNode(path, false, true) 25 | if not node or not vnode.fs and not vrest then 26 | return 0 -- virtual directory 27 | end 28 | if node.fs and rest then 29 | return node.fs.lastModified(rest) 30 | end 31 | return 0 -- no such file or directory 32 | end 33 | 34 | function filesystem.mounts() 35 | local tmp = {} 36 | for path,node in pairs(filesystem.fstab) do 37 | table.insert(tmp, {node.fs,path}) 38 | end 39 | return function() 40 | local next = table.remove(tmp) 41 | if next then return table.unpack(next) end 42 | end 43 | end 44 | 45 | function filesystem.link(target, linkpath) 46 | checkArg(1, target, "string") 47 | checkArg(2, linkpath, "string") 48 | 49 | if filesystem.exists(linkpath) then 50 | return nil, "file already exists" 51 | end 52 | local linkpath_parent = filesystem.path(linkpath) 53 | if not filesystem.exists(linkpath_parent) then 54 | return nil, "no such directory" 55 | end 56 | local linkpath_real, reason = filesystem.realPath(linkpath_parent) 57 | if not linkpath_real then 58 | return nil, reason 59 | end 60 | if not filesystem.isDirectory(linkpath_real) then 61 | return nil, "not a directory" 62 | end 63 | 64 | local _, _, vnode, _ = filesystem.findNode(linkpath_real, true) 65 | vnode.links[filesystem.name(linkpath)] = target 66 | return true 67 | end 68 | 69 | function filesystem.umount(fsOrPath) 70 | checkArg(1, fsOrPath, "string", "table") 71 | local real 72 | local fs 73 | local addr 74 | if type(fsOrPath) == "string" then 75 | real = filesystem.realPath(fsOrPath) 76 | addr = fsOrPath 77 | else -- table 78 | fs = fsOrPath 79 | end 80 | 81 | local paths = {} 82 | for path,node in pairs(filesystem.fstab) do 83 | if real == path or addr == node.fs.address or fs == node.fs then 84 | table.insert(paths, path) 85 | end 86 | end 87 | for _,path in ipairs(paths) do 88 | local node = filesystem.fstab[path] 89 | filesystem.fstab[path] = nil 90 | node.fs = nil 91 | node.parent.children[node.name] = nil 92 | end 93 | return #paths > 0 94 | end 95 | 96 | function filesystem.size(path) 97 | local node, rest, vnode, vrest = filesystem.findNode(path, false, true) 98 | if not node or not vnode.fs and (not vrest or vnode.links[vrest]) then 99 | return 0 -- virtual directory or symlink 100 | end 101 | if node.fs and rest then 102 | return node.fs.size(rest) 103 | end 104 | return 0 -- no such file or directory 105 | end 106 | 107 | function filesystem.isLink(path) 108 | local name = filesystem.name(path) 109 | local node, rest, vnode, vrest = filesystem.findNode(filesystem.path(path), false, true) 110 | if not node then return nil, rest end 111 | local target = vnode.links[name] 112 | -- having vrest here indicates we are not at the 113 | -- owning vnode due to a mount point above this point 114 | -- but we can have a target when there is a link at 115 | -- the mount point root, with the same name 116 | if not vrest and target ~= nil then 117 | return true, target 118 | end 119 | return false 120 | end 121 | 122 | function filesystem.copy(fromPath, toPath) 123 | local data = false 124 | local input, reason = filesystem.open(fromPath, "rb") 125 | if input then 126 | local output = filesystem.open(toPath, "wb") 127 | if output then 128 | repeat 129 | data, reason = input:read(1024) 130 | if not data then break end 131 | data, reason = output:write(data) 132 | if not data then data, reason = false, "failed to write" end 133 | until not data 134 | output:close() 135 | end 136 | input:close() 137 | end 138 | return data == nil, reason 139 | end 140 | 141 | local function readonly_wrap(proxy) 142 | checkArg(1, proxy, "table") 143 | if proxy.isReadOnly() then 144 | return proxy 145 | end 146 | 147 | local function roerr() return nil, "filesystem is readonly" end 148 | return setmetatable({ 149 | rename = roerr, 150 | open = function(path, mode) 151 | checkArg(1, path, "string") 152 | checkArg(2, mode, "string") 153 | if mode:match("[wa]") then 154 | return roerr() 155 | end 156 | return proxy.open(path, mode) 157 | end, 158 | isReadOnly = function() 159 | return true 160 | end, 161 | write = roerr, 162 | setLabel = roerr, 163 | makeDirectory = roerr, 164 | remove = roerr, 165 | }, {__index=proxy}) 166 | end 167 | 168 | local function bind_proxy(path) 169 | local real, reason = filesystem.realPath(path) 170 | if not real then 171 | return nil, reason 172 | end 173 | if not filesystem.isDirectory(real) then 174 | return nil, "must bind to a directory" 175 | end 176 | local real_fs, real_fs_path = filesystem.get(real) 177 | if real == real_fs_path then 178 | return real_fs 179 | end 180 | -- turn /tmp/foo into foo 181 | local rest = real:sub(#real_fs_path + 1) 182 | local function wrap_relative(fp) 183 | return function(mpath, ...) 184 | return fp(filesystem.concat(rest, mpath), ...) 185 | end 186 | end 187 | local bind = { 188 | type = "filesystem_bind", 189 | address = real, 190 | isReadOnly = real_fs.isReadOnly, 191 | list = wrap_relative(real_fs.list), 192 | isDirectory = wrap_relative(real_fs.isDirectory), 193 | size = wrap_relative(real_fs.size), 194 | lastModified = wrap_relative(real_fs.lastModified), 195 | exists = wrap_relative(real_fs.exists), 196 | open = wrap_relative(real_fs.open), 197 | remove = wrap_relative(real_fs.remove), 198 | read = real_fs.read, 199 | write = real_fs.write, 200 | close = real_fs.close, 201 | getLabel = function() return "" end, 202 | setLabel = function() return nil, "cannot set the label of a bind point" end, 203 | } 204 | return bind 205 | end 206 | 207 | filesystem.internal = {} 208 | function filesystem.internal.proxy(filter, options) 209 | checkArg(1, filter, "string") 210 | checkArg(2, options, "table", "nil") 211 | options = options or {} 212 | local address, proxy, reason 213 | if options.bind then 214 | proxy, reason = bind_proxy(filter) 215 | else 216 | -- no options: filter should be a label or partial address 217 | for c in component.list("filesystem", true) do 218 | if component.invoke(c, "getLabel") == filter then 219 | address = c 220 | break 221 | end 222 | if c:sub(1, filter:len()) == filter then 223 | address = c 224 | break 225 | end 226 | end 227 | if not address then 228 | return nil, "no such file system" 229 | end 230 | proxy, reason = component.proxy(address) 231 | end 232 | if not proxy then 233 | return proxy, reason 234 | end 235 | if options.readonly then 236 | proxy = readonly_wrap(proxy) 237 | end 238 | return proxy 239 | end 240 | 241 | function filesystem.remove(path) 242 | local function removeVirtual() 243 | local _, _, vnode, vrest = filesystem.findNode(filesystem.path(path), false, true) 244 | -- vrest represents the remaining path beyond vnode 245 | -- vrest is nil if vnode reaches the full path 246 | -- thus, if vrest is NOT NIL, then we SHOULD NOT remove children nor links 247 | if not vrest then 248 | local name = filesystem.name(path) 249 | if vnode.children[name] or vnode.links[name] then 250 | vnode.children[name] = nil 251 | vnode.links[name] = nil 252 | while vnode and vnode.parent and not vnode.fs and not next(vnode.children) and not next(vnode.links) do 253 | vnode.parent.children[vnode.name] = nil 254 | vnode = vnode.parent 255 | end 256 | return true 257 | end 258 | end 259 | -- return false even if vrest is nil because this means it was a expected 260 | -- to be a real file 261 | return false 262 | end 263 | local function removePhysical() 264 | local node, rest = filesystem.findNode(path) 265 | if node.fs and rest then 266 | return node.fs.remove(rest) 267 | end 268 | return false 269 | end 270 | local success = removeVirtual() 271 | success = removePhysical() or success -- Always run. 272 | if success then return true 273 | else return nil, "no such file or directory" 274 | end 275 | end 276 | 277 | function filesystem.rename(oldPath, newPath) 278 | if filesystem.isLink(oldPath) then 279 | local _, _, vnode, _ = filesystem.findNode(filesystem.path(oldPath)) 280 | local target = vnode.links[filesystem.name(oldPath)] 281 | local result, reason = filesystem.link(target, newPath) 282 | if result then 283 | filesystem.remove(oldPath) 284 | end 285 | return result, reason 286 | else 287 | local oldNode, oldRest = filesystem.findNode(oldPath) 288 | local newNode, newRest = filesystem.findNode(newPath) 289 | if oldNode.fs and oldRest and newNode.fs and newRest then 290 | if oldNode.fs.address == newNode.fs.address then 291 | return oldNode.fs.rename(oldRest, newRest) 292 | else 293 | local result, reason = filesystem.copy(oldPath, newPath) 294 | if result then 295 | return filesystem.remove(oldPath) 296 | else 297 | return nil, reason 298 | end 299 | end 300 | end 301 | return nil, "trying to read from or write to virtual directory" 302 | end 303 | end 304 | 305 | local isAutorunEnabled = nil 306 | local function saveConfig() 307 | local root = filesystem.get("/") 308 | if root and not root.isReadOnly() then 309 | local f = filesystem.open("/etc/filesystem.cfg", "w") 310 | if f then 311 | f:write("autorun="..tostring(isAutorunEnabled)) 312 | f:close() 313 | end 314 | end 315 | end 316 | 317 | function filesystem.isAutorunEnabled() 318 | if isAutorunEnabled == nil then 319 | local env = {} 320 | local config = loadfile("/etc/filesystem.cfg", nil, env) 321 | if config then 322 | pcall(config) 323 | isAutorunEnabled = not not env.autorun 324 | else 325 | isAutorunEnabled = true 326 | end 327 | saveConfig() 328 | end 329 | return isAutorunEnabled 330 | end 331 | 332 | function filesystem.setAutorunEnabled(value) 333 | checkArg(1, value, "boolean") 334 | isAutorunEnabled = value 335 | saveConfig() 336 | end 337 | 338 | -- -- luacheck: globals os 339 | -- os.remove = filesystem.remove 340 | -- os.rename = filesystem.rename 341 | 342 | -- os.execute = function(command) 343 | -- if not command then 344 | -- return type(shell) == "table" 345 | -- end 346 | -- return shell.execute(command) 347 | -- end 348 | 349 | -- function os.exit(code) 350 | -- error({reason="terminated", code=code}, 0) 351 | -- end 352 | 353 | -- function os.tmpname() 354 | -- local path = os.getenv("TMPDIR") or "/tmp" 355 | -- if filesystem.exists(path) then 356 | -- for _ = 1, 10 do 357 | -- local name = filesystem.concat(path, tostring(math.random(1, 0x7FFFFFFF))) 358 | -- if not filesystem.exists(name) then 359 | -- return name 360 | -- end 361 | -- end 362 | -- end 363 | -- end 364 | -------------------------------------------------------------------------------- /src/3rd/tty.lua: -------------------------------------------------------------------------------- 1 | -- object-based tty streams -- 2 | 3 | do 4 | local color_profiles = { 5 | { -- default VGA colors 6 | 0x000000, 7 | 0xaa0000, 8 | 0x00aa00, 9 | 0xaa5500, 10 | 0x0000aa, 11 | 0xaa00aa, 12 | 0x00aaaa, 13 | 0xaaaaaa, 14 | 0x555555, 15 | 0xff5555, 16 | 0x55ff55, 17 | 0xffff55, 18 | 0x5555ff, 19 | 0xff55ff, 20 | 0x55ffff, 21 | 0xffffff 22 | }, 23 | { -- Breeze theme colors from Konsole 24 | 0x232627, 25 | 0xed1515, 26 | 0x11d116, 27 | 0xf67400, 28 | 0x1d99f3, 29 | 0x9b59b6, 30 | 0x1abc9c, 31 | 0xfcfcfc, 32 | -- intense variants 33 | 0x7f8c8d, 34 | 0xc0392b, 35 | 0x1cdc9a, 36 | 0xfdbc4b, 37 | 0x3daee9, 38 | 0x8e44ad, 39 | 0x16a085, 40 | 0xffffff 41 | }, 42 | { -- Gruvbox 43 | 0x282828, 44 | 0xcc241d, 45 | 0x98971a, 46 | 0xd79921, 47 | 0x458588, 48 | 0xb16286, 49 | 0x689d6a, 50 | 0xa89984, 51 | 0x928374, 52 | 0xfb4934, 53 | 0xb8bb26, 54 | 0xfabd2f, 55 | 0x83a598, 56 | 0xd3869b, 57 | 0x8ec07c, 58 | 0xebdbb2 59 | }, 60 | { -- Gruvbox light, for those crazy enough to want a light theme 61 | 0xfbf1c7, 62 | 0xcc241d, 63 | 0x98971a, 64 | 0xd79921, 65 | 0x458588, 66 | 0xb16286, 67 | 0x689d6a, 68 | 0x7c6f64, 69 | 0x928374, 70 | 0x9d0006, 71 | 0x79740e, 72 | 0xb57614, 73 | 0x076678, 74 | 0x8f3f71, 75 | 0x427b58, 76 | 0x3c3836 77 | }, 78 | { -- PaperColor light 79 | 0xeeeeee, 80 | 0xaf0000, 81 | 0x008700, 82 | 0x5f8700, 83 | 0x0087af, 84 | 0x878787, 85 | 0x005f87, 86 | 0x444444, 87 | 0xbcbcbc, 88 | 0xd70000, 89 | 0xd70087, 90 | 0x8700af, 91 | 0xd75f00, 92 | 0xd75f00, 93 | 0x005faf, 94 | 0x005f87 95 | }, 96 | { -- Pale Night 97 | 0x292d3e, 98 | 0xf07178, 99 | 0xc3e88d, 100 | 0xffcb6b, 101 | 0x82aaff, 102 | 0xc792ea, 103 | 0x89ddff, 104 | 0xd0d0d0, 105 | 0x434758, 106 | 0xff8b92, 107 | 0xddffa7, 108 | 0xffe585, 109 | 0x9cc4ff, 110 | 0xe1acff, 111 | 0xa3f7ff, 112 | 0xffffff, 113 | } 114 | } 115 | local colors = color_profiles[1] 116 | 117 | local len = unicode.len 118 | local sub = unicode.sub 119 | 120 | -- pop characters from the end of a string 121 | local function pop(str, n, u) 122 | local sub, len = string.sub, string.len 123 | if not u then sub = unicode.sub len = unicode.len end 124 | local ret = sub(str, 1, n) 125 | local also = sub(str, len(ret) + 1, -1) 126 | 127 | return also, ret 128 | end 129 | 130 | local function wrap_cursor(self) 131 | while self.cx > self.w do 132 | --if self.cx > self.w then 133 | self.cx, self.cy = math.max(1, self.cx - self.w), self.cy + 1 134 | end 135 | 136 | while self.cx < 1 do 137 | self.cx, self.cy = self.w + self.cx, self.cy - 1 138 | end 139 | 140 | while self.cy < 1 do 141 | self.cy = self.cy + 1 142 | self.gpu.copy(1, 1, self.w, self.h - 1, 0, 1) 143 | self.gpu.fill(1, 1, self.w, 1, " ") 144 | end 145 | 146 | while self.cy > self.h do 147 | self.cy = self.cy - 1 148 | self.gpu.copy(1, 2, self.w, self.h, 0, -1) 149 | self.gpu.fill(1, self.h, self.w, 1, " ") 150 | end 151 | end 152 | 153 | local function writeline(self, rline) 154 | local wrapped = false 155 | while #rline > 0 do 156 | local to_write 157 | rline, to_write = pop(rline, self.w - self.cx + 1) 158 | 159 | self.gpu.set(self.cx, self.cy, to_write) 160 | 161 | self.cx = self.cx + len(to_write) 162 | wrapped = self.cx > self.w 163 | 164 | wrap_cursor(self) 165 | end 166 | return wrapped 167 | end 168 | 169 | local function write(self, lines) 170 | if self.attributes.xoff then return end 171 | while #lines > 0 do 172 | local next_nl = lines:find("\n") 173 | 174 | if next_nl then 175 | local ln 176 | lines, ln = pop(lines, next_nl - 1, true) 177 | lines = lines:sub(2) -- take off the newline 178 | 179 | local w = writeline(self, ln) 180 | 181 | if not w then 182 | self.cx, self.cy = 1, self.cy + 1 183 | end 184 | 185 | wrap_cursor(self) 186 | else 187 | writeline(self, lines) 188 | break 189 | end 190 | end 191 | end 192 | 193 | local commands, control = {}, {} 194 | local separators = { 195 | standard = "[", 196 | control = "?" 197 | } 198 | 199 | -- move cursor up N[=1] lines 200 | function commands:A(args) 201 | local n = math.max(args[1] or 0, 1) 202 | self.cy = self.cy - n 203 | end 204 | 205 | -- move cursor down N[=1] lines 206 | function commands:B(args) 207 | local n = math.max(args[1] or 0, 1) 208 | self.cy = self.cy + n 209 | end 210 | 211 | -- move cursor right N[=1] lines 212 | function commands:C(args) 213 | local n = math.max(args[1] or 0, 1) 214 | self.cx = self.cx + n 215 | end 216 | 217 | -- move cursor left N[=1] lines 218 | function commands:D(args) 219 | local n = math.max(args[1] or 0, 1) 220 | self.cx = self.cx - n 221 | end 222 | 223 | -- incompatibility: terminal-specific command for calling advanced GPU 224 | -- functionality 225 | function commands:g(args) 226 | if #args < 1 then return end 227 | local cmd = table.remove(args, 1) 228 | if cmd == 0 then -- fill 229 | if #args < 4 then return end 230 | args[1] = math.max(1, math.min(args[1], self.w)) 231 | args[2] = math.max(1, math.min(args[2], self.h)) 232 | self.gpu.fill(args[1], args[2], args[3], args[4], " ") 233 | elseif cmd == 1 then -- copy 234 | if #args < 6 then return end 235 | self.gpu.copy(args[1], args[2], args[3], args[4], args[5], args[6]) 236 | end 237 | -- TODO more commands 238 | end 239 | 240 | function commands:G(args) 241 | self.cx = math.max(1, math.min(self.w, args[1] or 1)) 242 | end 243 | 244 | function commands:H(args) 245 | local y, x = 1, 1 246 | y = args[1] or y 247 | x = args[2] or x 248 | 249 | self.cx = math.max(1, math.min(self.w, x)) 250 | self.cy = math.max(1, math.min(self.h, y)) 251 | 252 | wrap_cursor(self) 253 | end 254 | 255 | -- clear a portion of the screen 256 | function commands:J(args) 257 | local n = args[1] or 0 258 | 259 | if n == 0 then 260 | self.gpu.fill(1, self.cy, self.w, self.h - self.cy, " ") 261 | elseif n == 1 then 262 | self.gpu.fill(1, 1, self.w, self.cy, " ") 263 | elseif n == 2 then 264 | self.gpu.fill(1, 1, self.w, self.h, " ") 265 | end 266 | end 267 | 268 | -- clear a portion of the current line 269 | function commands:K(args) 270 | local n = args[1] or 0 271 | 272 | if n == 0 then 273 | self.gpu.fill(self.cx, self.cy, self.w, 1, " ") 274 | elseif n == 1 then 275 | self.gpu.fill(1, self.cy, self.cx, 1, " ") 276 | elseif n == 2 then 277 | self.gpu.fill(1, self.cy, self.w, 1, " ") 278 | end 279 | end 280 | 281 | -- adjust some terminal attributes - foreground/background color and local 282 | -- echo. for more control {ESC}?c may be desirable. 283 | function commands:m(args) 284 | args[1] = args[1] or 0 285 | local i = 1 286 | while i <= #args do 287 | local n = args[i] 288 | if n == 0 then 289 | self.fg = 7 290 | self.bg = 0 291 | self.fgp = true 292 | self.bgp = true 293 | self.gpu.setForeground(self.fg, true) 294 | self.gpu.setBackground(self.bg, true) 295 | self.attributes.echo = true 296 | elseif n == 8 then 297 | self.attributes.echo = false 298 | elseif n == 28 then 299 | self.attributes.echo = true 300 | elseif n > 29 and n < 38 then 301 | self.fg = n - 30 302 | self.fgp = true 303 | self.gpu.setForeground(self.fg, true) 304 | elseif n == 39 then 305 | self.fg = 7 306 | self.fgp = true 307 | self.gpu.setForeground(self.fg, true) 308 | elseif n > 39 and n < 48 then 309 | self.bg = n - 40 310 | self.bgp = true 311 | self.gpu.setBackground(self.bg, true) 312 | elseif n == 49 then 313 | self.bg = 0 314 | self.bgp = true 315 | self.gpu.setBackground(self.bg, true) 316 | elseif n > 89 and n < 98 then 317 | self.fg = n - 82 318 | self.fgp = true 319 | self.gpu.setForeground(self.fg, true) 320 | elseif n > 99 and n < 108 then 321 | self.bg = n - 92 322 | self.bgp = true 323 | self.gpu.setBackground(self.bg, true) 324 | elseif n == 38 then 325 | i = i + 1 326 | if not args[i] then return end 327 | local mode = args[i] 328 | if mode == 5 then -- 256-color mode 329 | -- TODO 330 | elseif mode == 2 then -- 24-bit color mode 331 | local r, g, b = args[i + 1], args[i + 2], args[i + 3] 332 | if not b then return end 333 | i = i + 3 334 | self.fg = (r << 16) + (g << 8) + b 335 | self.fgp = false 336 | self.gpu.setForeground(self.fg) 337 | end 338 | elseif n == 48 then 339 | i = i + 1 340 | if not args[i] then return end 341 | local mode = args[i] 342 | if mode == 5 then -- 256-color mode 343 | -- TODO 344 | elseif mode == 2 then -- 24-bit color mode 345 | local r, g, b = args[i + 1], args[i + 2], args[i + 3] 346 | if not b then return end 347 | i = i + 3 348 | self.bg = (r << 16) + (g << 8) + b 349 | self.bgp = false 350 | self.gpu.setBackground(self.bg) 351 | end 352 | end 353 | i = i + 1 354 | end 355 | end 356 | 357 | function commands:n(args) 358 | local n = args[1] or 0 359 | 360 | if n == 6 then 361 | self.rb = string.format("%s\27[%d;%dR", self.rb, self.cy, self.cx) 362 | end 363 | end 364 | 365 | function commands:S(args) 366 | local n = args[1] or 1 367 | self.gpu.copy(1, n, self.w, self.h, 0, -n) 368 | self.gpu.fill(1, self.h - n, self.w, n, " ") 369 | end 370 | 371 | function commands:T(args) 372 | local n = args[1] or 1 373 | self.gpu.copy(1, 1, self.w, self.h-n, 0, n) 374 | self.gpu.fill(1, 1, self.w, n, " ") 375 | end 376 | 377 | -- adjust more terminal attributes 378 | -- codes: 379 | -- - 0: reset 380 | -- - 1: enable echo 381 | -- - 2: enable line mode 382 | -- - 3: enable raw mode 383 | -- - 4: show cursor 384 | -- - 5: undo 15 385 | -- - 11: disable echo 386 | -- - 12: disable line mode 387 | -- - 13: disable raw mode 388 | -- - 14: hide cursor 389 | -- - 15: disable all input and output 390 | function control:c(args) 391 | args[1] = args[1] or 0 392 | 393 | for i=1, #args, 1 do 394 | local n = args[i] 395 | 396 | if n == 0 then -- (re)set configuration to sane defaults 397 | -- echo text that the user has entered? 398 | self.attributes.echo = true 399 | 400 | -- buffer input by line? 401 | self.attributes.line = true 402 | 403 | -- whether to send raw key input data according to the VT100 spec, 404 | -- rather than e.g. changing \r -> \n and capturing backspace 405 | self.attributes.raw = false 406 | 407 | -- whether to show the terminal cursor 408 | self.attributes.cursor = true 409 | elseif n == 1 then 410 | self.attributes.echo = true 411 | elseif n == 2 then 412 | self.attributes.line = true 413 | elseif n == 3 then 414 | self.attributes.raw = true 415 | elseif n == 4 then 416 | self.attributes.cursor = true 417 | elseif n == 5 then 418 | self.attributes.xoff = false 419 | elseif n == 11 then 420 | self.attributes.echo = false 421 | elseif n == 12 then 422 | self.attributes.line = false 423 | elseif n == 13 then 424 | self.attributes.raw = false 425 | elseif n == 14 then 426 | self.attributes.cursor = false 427 | elseif n == 15 then 428 | self.attributes.xoff = true 429 | end 430 | end 431 | end 432 | 433 | -- adjust signal behavior 434 | -- 0: reset 435 | -- 1: disable INT on ^C 436 | -- 2: disable keyboard STOP on ^Z 437 | -- 3: disable HUP on ^D 438 | -- 11: enable INT 439 | -- 12: enable STOP 440 | -- 13: enable HUP 441 | function control:s(args) 442 | args[1] = args[1] or 0 443 | for i=1, #args, 1 do 444 | local n = args[i] 445 | if n == 0 then 446 | self.disabled = {} 447 | elseif n == 1 then 448 | self.disabled.C = true 449 | elseif n == 2 then 450 | self.disabled.Z = true 451 | elseif n == 3 then 452 | self.disabled.D = true 453 | elseif n == 11 then 454 | self.disabled.C = false 455 | elseif n == 12 then 456 | self.disabled.Z = false 457 | elseif n == 13 then 458 | self.disabled.D = false 459 | end 460 | end 461 | end 462 | 463 | local _stream = {} 464 | 465 | local function temp(...) 466 | return ... 467 | end 468 | 469 | function _stream:write(...) 470 | checkArg(1, ..., "string") 471 | 472 | local str = table.concat({...}, "") 473 | 474 | if self.attributes.line then 475 | self.wb = self.wb .. str 476 | if self.wb:find("\n") then 477 | local ln = self.wb:match(".+\n") 478 | if not ln then ln = self.wb:match(".-\n") end 479 | self.wb = self.wb:sub(#ln + 1) 480 | return self:write_str(ln) 481 | elseif len(self.wb) > 2048 then 482 | local ln = self.wb 483 | self.wb = "" 484 | return self:write_str(ln) 485 | end 486 | else 487 | return self:write_str(str) 488 | end 489 | end 490 | 491 | -- This is where most of the heavy lifting happens. I've attempted to make 492 | -- this function fairly optimized, but there's only so much one can do given 493 | -- OpenComputers's call budget limits and wrapped string library. 494 | function _stream:write_str(str) 495 | local gpu = self.gpu 496 | local time = computer.uptime() 497 | 498 | -- TODO: cursor logic is a bit brute-force currently, there are certain 499 | -- TODO: scenarios where cursor manipulation is unnecessary 500 | if self.attributes.cursor then 501 | local c, f, b, pf, pb = gpu.get(self.cx, self.cy) 502 | if pf then 503 | gpu.setForeground(pb, true) 504 | gpu.setBackground(pf, true) 505 | else 506 | gpu.setForeground(b) 507 | gpu.setBackground(f) 508 | end 509 | gpu.set(self.cx, self.cy, c) 510 | gpu.setForeground(self.fg, self.fgp) 511 | gpu.setBackground(self.bg, self.bgp) 512 | end 513 | 514 | -- lazily convert tabs 515 | str = str:gsub("\t", " ") 516 | 517 | while #str > 0 do 518 | --[[if computer.uptime() - time >= 4.8 then -- almost TLWY 519 | time = computer.uptime() 520 | computer.pullSignal(0) -- yield so we don't die 521 | end]] 522 | 523 | if self.in_esc then 524 | local esc_end = str:find("[a-zA-Z]") 525 | 526 | if not esc_end then 527 | self.esc = self.esc .. str 528 | else 529 | self.in_esc = false 530 | 531 | local finish 532 | str, finish = pop(str, esc_end, true) 533 | 534 | local esc = self.esc .. finish 535 | self.esc = "" 536 | 537 | local separator, raw_args, code = esc:match( 538 | "\27([%[%?])([%-%d;]*)([a-zA-Z])") 539 | raw_args = raw_args or "0" 540 | 541 | local args = {} 542 | for arg in raw_args:gmatch("([^;]+)") do 543 | args[#args + 1] = tonumber(arg) or 0 544 | end 545 | 546 | if separator == separators.standard and commands[code] then 547 | commands[code](self, args) 548 | elseif separator == separators.control and control[code] then 549 | control[code](self, args) 550 | end 551 | 552 | wrap_cursor(self) 553 | end 554 | else 555 | -- handle BEL and \r 556 | if str:find("\a") then 557 | computer.beep() 558 | end 559 | str = str:gsub("\a", "") 560 | str = str:gsub("\r", "\27[G") 561 | 562 | local next_esc = str:find("\27") 563 | 564 | if next_esc then 565 | self.in_esc = true 566 | self.esc = "" 567 | 568 | local ln 569 | str, ln = pop(str, next_esc - 1, true) 570 | 571 | write(self, ln) 572 | else 573 | write(self, str) 574 | str = "" 575 | end 576 | end 577 | end 578 | 579 | if self.attributes.cursor then 580 | local c, f, b, pf, pb = gpu.get(self.cx, self.cy) 581 | 582 | if pf then 583 | gpu.setForeground(pb, true) 584 | gpu.setBackground(pf, true) 585 | else 586 | gpu.setForeground(b) 587 | gpu.setBackground(f) 588 | end 589 | gpu.set(self.cx, self.cy, c) 590 | if pf then 591 | gpu.setForeground(self.fg, self.fgp) 592 | gpu.setBackground(self.bg, self.bgp) 593 | end 594 | end 595 | 596 | return true 597 | end 598 | 599 | function _stream:flush() 600 | if #self.wb > 0 then 601 | self:write_str(self.wb) 602 | self.wb = "" 603 | end 604 | return true 605 | end 606 | 607 | -- aliases of key scan codes to key inputs 608 | local aliases = { 609 | [200] = "\27[A", -- up 610 | [208] = "\27[B", -- down 611 | [205] = "\27[C", -- right 612 | [203] = "\27[D", -- left 613 | } 614 | 615 | local sigacts = { 616 | D = 1, -- hangup, TODO: check this is correct 617 | C = 2, -- interrupt 618 | Z = 18, -- keyboard stop 619 | } 620 | 621 | function _stream:key_down(...) 622 | local signal = table.pack(...) 623 | 624 | if not self.keyboards[signal[2]] then 625 | return 626 | end 627 | 628 | if signal[3] == 0 and signal[4] == 0 then 629 | return 630 | end 631 | 632 | if self.xoff then 633 | return 634 | end 635 | 636 | local char = aliases[signal[4]] or 637 | (signal[3] > 255 and unicode.char or string.char)(signal[3]) 638 | local ch = signal[3] 639 | local tw = char 640 | 641 | if ch == 0 and not aliases[signal[4]] then 642 | return 643 | end 644 | 645 | if len(char) == 1 and ch == 0 then 646 | char = "" 647 | tw = "" 648 | elseif char:match("\27%[[ABCD]") then 649 | tw = string.format("^[%s", char:sub(-1)) 650 | elseif #char == 1 and ch < 32 then 651 | local tch = string.char( 652 | (ch == 0 and 32) or 653 | (ch < 27 and ch + 96) or 654 | (ch == 27 and 91) or -- [ 655 | (ch == 28 and 92) or -- \ 656 | (ch == 29 and 93) or -- ] 657 | (ch == 30 and 126) or 658 | (ch == 31 and 63) or ch 659 | ):upper() 660 | 661 | if sigacts[tch] and not self.disabled[tch] and kernel.scheduler.threads 662 | and not self.attributes.raw then 663 | -- fairly stupid method of determining the foreground process: 664 | -- find the highest PID associated with this TTY 665 | -- yeah, it's stupid, but it should work in most cases. 666 | -- and where it doesn't the shell should handle it. 667 | local mxp = 0 668 | 669 | for _k, v in pairs(kernel.scheduler.threads) do 670 | --k.log(k.loglevels.error, _k, v.name, v.io.stderr.tty, self.ttyn) 671 | if v.stderr == self.tty then 672 | mxp = math.max(mxp, _k) 673 | elseif v.stdin == self.tty then 674 | mxp = math.max(mxp, _k) 675 | elseif v.stdout == self.tty then 676 | mxp = math.max(mxp, _k) 677 | end 678 | end 679 | 680 | --k.log(k.loglevels.error, "sending", sigacts[tch], "to", mxp == 0 and mxp or k.scheduler.processes[mxp].name) 681 | 682 | if mxp > 0 then 683 | kernel.scheduler.kill(mxp, sigacts[tch]) 684 | end 685 | 686 | self.rb = "" 687 | if tch == "\4" then self.rb = tch end 688 | char = "" 689 | end 690 | 691 | tw = "^" .. tch 692 | end 693 | 694 | if not self.attributes.raw then 695 | if ch == 13 then 696 | char = "\n" 697 | tw = "\n" 698 | elseif ch == 8 then 699 | if #self.rb > 0 then 700 | tw = "\27[D \27[D" 701 | self.rb = self.rb:sub(1, -2) 702 | else 703 | tw = "" 704 | end 705 | char = "" 706 | end 707 | end 708 | 709 | if self.attributes.echo and not self.attributes.xoff then 710 | self:write_str(tw or "") 711 | end 712 | 713 | if not self.attributes.xoff then 714 | self.rb = self.rb .. char 715 | end 716 | end 717 | 718 | function _stream:clipboard(...) 719 | local signal = table.pack(...) 720 | 721 | for c in signal[3]:gmatch(".") do 722 | self:key_down(signal[1], signal[2], c:byte(), 0) 723 | end 724 | end 725 | 726 | function _stream:read(n) 727 | checkArg(1, n, "number") 728 | 729 | self:flush() 730 | 731 | local dd = self.disabled.D or self.attributes.raw 732 | 733 | if self.attributes.line then 734 | while (not self.rb:find("\n")) or (len(self.rb:sub(1, (self.rb:find("\n")))) < n) 735 | and not (self.rb:find("\4") and not dd) do 736 | coroutine.yield() 737 | end 738 | else 739 | while len(self.rb) < n and (self.attributes.raw or not 740 | (self.rb:find("\4") and not dd)) do 741 | coroutine.yield() 742 | end 743 | end 744 | 745 | if self.rb:find("\4") and not dd then 746 | self.rb = "" 747 | return nil 748 | end 749 | 750 | local data = sub(self.rb, 1, n) 751 | self.rb = sub(self.rb, n + 1) 752 | return data 753 | end 754 | 755 | local function closed() 756 | return nil, "stream closed" 757 | end 758 | 759 | function _stream:close() 760 | self:flush() 761 | self.closed = true 762 | self.read = closed 763 | self.write = closed 764 | self.flush = closed 765 | self.close = closed 766 | -- k.event.unregister(self.key_handler_id) 767 | -- k.event.unregister(self.clip_handler_id) 768 | kernel.unregister_hook("key_down", self.key_handler_id) 769 | kernel.unregister_hook("clipboard", self.clip_handler_id) 770 | kernel.unregister_hook("timer", self.timer_id) 771 | -- if self.ttyn then kernel.unregister_chrdev("/dev/tty"..self.ttyn) end 772 | if self.ttyn then kernel.unregister_chrdev("tty"..self.ttyn) end 773 | return true 774 | end 775 | 776 | local ttyn = 0 777 | 778 | -- this is the raw function for creating TTYs over components 779 | -- userspace gets somewhat-abstracted-away stuff 780 | function kernel.create_tty(gpu, screen) 781 | checkArg(1, gpu, "string", "table") 782 | checkArg(2, screen, "string", "nil") 783 | 784 | local proxy 785 | if type(gpu) == "string" then 786 | proxy = component.proxy(gpu) 787 | 788 | if screen then proxy.bind(screen) end 789 | else 790 | proxy = gpu 791 | end 792 | 793 | -- set the gpu's palette 794 | for i=1, #colors, 1 do 795 | proxy.setPaletteColor(i - 1, colors[i]) 796 | end 797 | 798 | proxy.setForeground(7, true) 799 | proxy.setBackground(0, true) 800 | 801 | proxy.setDepth(proxy.maxDepth()) 802 | -- optimizations for no color on T1 803 | if proxy.getDepth() == 1 then 804 | local fg, bg = proxy.setForeground, proxy.setBackground 805 | local f, b = 7, 0 806 | function proxy.setForeground(c) 807 | -- [[ 808 | if c >= 0xAAAAAA or c <= 0x000000 and f ~= c then 809 | fg(c) 810 | end 811 | f = c 812 | --]] 813 | end 814 | function proxy.setBackground(c) 815 | -- [[ 816 | if c >= 0xDDDDDD or c <= 0x000000 and b ~= c then 817 | bg(c) 818 | end 819 | b = c 820 | --]] 821 | end 822 | proxy.getBackground = function()return f end 823 | proxy.getForeground = function()return b end 824 | end 825 | 826 | -- userspace will never directly see this, so it doesn't really matter what 827 | -- we put in this table 828 | local new = setmetatable({ 829 | attributes = {echo=true,line=true,raw=false,cursor=false,xoff=false}, -- terminal attributes 830 | disabled = {}, -- disabled signals 831 | keyboards = {}, -- all attached keyboards on terminal initialization 832 | in_esc = false, -- was a partial escape sequence written 833 | gpu = proxy, -- the associated GPU 834 | esc = "", -- the escape sequence buffer 835 | cx = 1, -- the cursor's X position 836 | cy = 1, -- the cursor's Y position 837 | fg = 7, -- the current foreground color 838 | bg = 0, -- the current background color 839 | fgp = true, -- whether the foreground color is a palette index 840 | bgp = true, -- whether the background color is a palette index 841 | rb = "", -- a buffer of characters read from the input 842 | wb = "", -- line buffering at its finest 843 | }, {__index = _stream}) 844 | 845 | -- avoid gpu.getResolution calls 846 | new.w, new.h = proxy.maxResolution() 847 | 848 | proxy.setResolution(new.w, new.h) 849 | proxy.fill(1, 1, new.w, new.h, " ") 850 | 851 | if screen then 852 | -- register all keyboards attached to the screen 853 | for _, keyboard in pairs(component.invoke(screen, "getKeyboards")) do 854 | new.keyboards[keyboard] = true 855 | end 856 | end 857 | 858 | -- register a keypress handler 859 | -- new.key_handler_id = k.event.register("key_down", function(...) 860 | -- return new:key_down(...) 861 | -- end) 862 | new.key_handler_id = kernel.register_hook("key_down", function(...) 863 | return new:key_down(...) 864 | end) 865 | 866 | -- new.clip_handler_id = k.event.register("clipboard", function(...) 867 | -- return new:clipboard(...) 868 | -- end) 869 | new.clip_handler_id = kernel.register_hook("clipboard", function(...) 870 | return new:clipboard(...) 871 | end) 872 | 873 | new.timer_id = kernel.register_hook("timer", function() 874 | return new:flush() 875 | end) 876 | 877 | -- register the TTY with the sysfs 878 | -- if k.sysfs then 879 | -- k.sysfs.register(k.sysfs.types.tty, new, "/dev/tty"..ttyn) 880 | -- new.ttyn = ttyn 881 | -- end 882 | kernel.register_chrdev("tty"..ttyn, { 883 | read = function(...) 884 | return new:read(...) 885 | end, 886 | write = function(...) 887 | return new:write(...) 888 | end, 889 | }) 890 | 891 | new.tty = ttyn 892 | 893 | -- if k.gpus then 894 | -- k.gpus[ttyn] = proxy 895 | -- end 896 | 897 | ttyn = ttyn + 1 898 | 899 | return new 900 | end 901 | end 902 | -------------------------------------------------------------------------------- /src/devfs.lua: -------------------------------------------------------------------------------- 1 | do 2 | ---@class DeviceFile 3 | ---@field read fun(count: integer): string 4 | ---@field write fun(data: string) 5 | 6 | --- List of device files. 7 | ---@type table 8 | local device_files = {} 9 | 10 | --- Register a device file. 11 | ---@param name string 12 | ---@param file DeviceFile 13 | function kernel.register_chrdev(name, file) 14 | assert(not name:match("/"), "device file name cannot contain '/'") 15 | assert(file, "file is nil") 16 | file.read = file.read or function() return "" end 17 | file.write = file.write or function() end 18 | 19 | device_files[name] = file 20 | end 21 | 22 | --- Unregister a device file. 23 | ---@param name string 24 | function kernel.unregister_chrdev(name) 25 | device_files[name] = nil 26 | end 27 | 28 | kernel.filesystem.mount({ 29 | open = function(path, mode) 30 | assert(not path:match("/"), "device file name cannot contain '/'") 31 | assert(device_files[path], "no such device file") 32 | return path 33 | end, 34 | read = function(path, count) 35 | assert(not path:match("/"), "device file name cannot contain '/'") 36 | assert(device_files[path], "no such device file") 37 | return device_files[path].read(count) 38 | end, 39 | write = function(path, data) 40 | assert(not path:match("/"), "device file name cannot contain '/'") 41 | assert(device_files[path], "no such device file") 42 | return device_files[path].write(data) 43 | end, 44 | exists = function(path) 45 | assert(not path:match("/"), "device file name cannot contain '/'") 46 | return device_files[path] ~= nil 47 | end, 48 | }, "/dev") 49 | end 50 | -------------------------------------------------------------------------------- /src/eventhooks.lua: -------------------------------------------------------------------------------- 1 | --- Event hooks are used internally by the kernel and kernel modules as an alternative to setting up threads to handle events. 2 | 3 | do 4 | ---@type table 5 | kernel.hooks = {} 6 | 7 | --- Register an event hook and return its ID. 8 | ---@param event string 9 | ---@param callback function 10 | function kernel.register_hook(event, callback) 11 | if not kernel.hooks[event] then 12 | kernel.hooks[event] = {} 13 | end 14 | kernel.hooks[event][#kernel.hooks[event] + 1] = callback 15 | return #kernel.hooks[event] 16 | end 17 | 18 | --- Unregister an event hook. 19 | ---@param event string 20 | ---@param id number 21 | function kernel.unregister_hook(event, id) 22 | assert(kernel.hooks[event], "no such event") 23 | assert(kernel.hooks[event][id], "no such hook") 24 | table.remove(kernel.hooks[event], id) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/filesystem.lua: -------------------------------------------------------------------------------- 1 | do 2 | --#include "3rd/filesystem.lua" "filesystem" 3 | kernel.syscalls.mount = filesystem.mount 4 | kernel.syscalls.umount = filesystem.umount 5 | 6 | ---@param path string 7 | ---@param mode string 8 | ---@return integer?, string? 9 | function kernel.syscalls.open(path, mode) 10 | local f, e = filesystem.open(path, mode) 11 | if f then 12 | local current_pid = kernel.scheduler.current_pid 13 | local fd = #kernel.scheduler.threads[current_pid].fd + 1 14 | kernel.scheduler.threads[current_pid].fd[fd] = f 15 | return fd, nil 16 | else 17 | return nil, e 18 | end 19 | end 20 | 21 | ---@param fd integer 22 | function kernel.syscalls.close(fd) 23 | local current_pid = kernel.scheduler.current_pid 24 | if not kernel.scheduler.threads[current_pid].fd[fd] then 25 | return nil, "bad file descriptor" 26 | end 27 | kernel.scheduler.threads[current_pid].fd[fd]:close() 28 | kernel.scheduler.threads[current_pid].fd[fd] = nil 29 | end 30 | 31 | ---@param fd integer 32 | ---@param count integer 33 | ---@return string?, string? 34 | function kernel.syscalls.read(fd, count) 35 | local current_pid = kernel.scheduler.current_pid 36 | if not kernel.scheduler.threads[current_pid].fd[fd] then 37 | return nil, "bad file descriptor" 38 | end 39 | return kernel.scheduler.threads[current_pid].fd[fd]:read(count) 40 | end 41 | 42 | ---@param fd integer 43 | ---@param data string 44 | ---@return boolean 45 | function kernel.syscalls.write(fd, data) 46 | local current_pid = kernel.scheduler.current_pid 47 | if not kernel.scheduler.threads[current_pid].fd[fd] then 48 | return nil, "bad file descriptor" 49 | end 50 | return kernel.scheduler.threads[current_pid].fd[fd]:write(data) 51 | end 52 | 53 | ---@param fd integer 54 | ---@param offset integer 55 | ---@param whence integer 56 | ---@return integer 57 | function kernel.syscalls.lseek(fd, offset, whence) 58 | local current_pid = kernel.scheduler.current_pid 59 | if not kernel.scheduler.threads[current_pid].fd[fd] then 60 | return nil, "bad file descriptor" 61 | end 62 | return kernel.scheduler.threads[current_pid].fd[fd]:lseek(offset, whence) 63 | end 64 | 65 | kernel.filesystem = filesystem 66 | end 67 | -------------------------------------------------------------------------------- /src/loadfile.lua: -------------------------------------------------------------------------------- 1 | ---@param path string 2 | ---@param env table 3 | ---@return function 4 | function kernel.loadfile(path, env) 5 | local f, e = kernel.filesystem.open(path, "r") 6 | if not f then 7 | return nil, e 8 | end 9 | 10 | local exe = "" 11 | repeat 12 | local data = f:read(math.huge) 13 | exe = exe .. (data or "") 14 | until not data 15 | f:close() 16 | 17 | local chunk, e = load(exe, "=" .. path, "t", env or {}) 18 | if not chunk then 19 | return nil, e 20 | end 21 | 22 | return chunk 23 | end 24 | -------------------------------------------------------------------------------- /src/main.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable-next-line: lowercase-global 2 | kernel = {} 3 | kernel.syscalls = {} 4 | 5 | --#include "eventhooks.lua" 6 | --#include "filesystem.lua" 7 | --#include "loadfile.lua" 8 | 9 | kernel.filesystem.mount(computer.getBootAddress(), "/") 10 | --#include "devfs.lua" 11 | 12 | --#include "3rd/tty.lua" 13 | do 14 | local gpu = component.proxy(component.list("gpu")()) 15 | kernel.create_tty(gpu, gpu.getScreen()) 16 | kernel.filesystem.link("/dev/tty0", "/dev/console") 17 | end 18 | 19 | --#include "printk.lua" 20 | --#include "scheduler.lua" 21 | 22 | function kernel.gen_env(...) 23 | local env = {} 24 | for k, v in pairs(_G) do 25 | if not ({ 26 | kernel = true, 27 | computer = true, 28 | component = true, 29 | })[k] then 30 | env[k] = v 31 | end 32 | end 33 | for _, addition in ipairs({...}) do 34 | for k, v in pairs(addition) do 35 | env[k] = v 36 | end 37 | end 38 | return env 39 | end 40 | 41 | function kernel.panic(fmt, ...) 42 | kernel.printk("\aKERNEL PANIC") 43 | if fmt then 44 | kernel.printk(": %s\n", fmt:format(...)) 45 | else 46 | kernel.printk("\n") 47 | end 48 | kernel.printk("System halted.\n") 49 | while true do 50 | computer.pullSignal() 51 | end 52 | end 53 | 54 | kernel.scheduler.spawn("/sbin/init.lua", "/sbin/init.lua") 55 | 56 | local last_signal = {} 57 | kernel.get_signal = setmetatable({}, { 58 | __call = function(self, sig) 59 | return table.unpack(last_signal) 60 | end 61 | }) 62 | 63 | local last_uptime = computer.uptime() 64 | repeat 65 | last_signal = {computer.pullSignal(0)} 66 | if kernel.hooks[last_signal[1]] and last_signal[1] ~= "timer" then 67 | for _, hook in ipairs(kernel.hooks[last_signal[1]]) do 68 | hook(table.unpack(last_signal)) 69 | end 70 | end 71 | if kernel.hooks.timer then 72 | for _, hook in ipairs(kernel.hooks.timer) do 73 | hook(computer.uptime() - last_uptime) 74 | end 75 | end 76 | last_uptime = computer.uptime() 77 | until not kernel.scheduler.kill(1, 0) 78 | 79 | while true do 80 | computer.pullSignal() 81 | end 82 | -------------------------------------------------------------------------------- /src/printk.lua: -------------------------------------------------------------------------------- 1 | do 2 | local console = kernel.filesystem.open("/dev/tty0", "w") 3 | 4 | function kernel.printk(fmt, ...) 5 | assert(type(fmt) == "string", "fmt is not a string") 6 | console:write(string.format(fmt, ...)) 7 | end 8 | end -------------------------------------------------------------------------------- /src/scheduler.lua: -------------------------------------------------------------------------------- 1 | -- Round-robin coroutine scheduler 2 | 3 | do 4 | ---@class Thread 5 | ---@field coroutine thread 6 | ---@field cwd string 7 | ---@field comm string 8 | ---@field cmdline string[] 9 | ---@field environ string[] 10 | ---@field fd table[] 11 | 12 | local scheduler = {} 13 | scheduler.threads = {} 14 | scheduler.current_pid = 0 15 | 16 | --- Spawn a new thread and return its PID. 17 | ---@param path string 18 | ---@param args string[] 19 | ---@param env string[] 20 | ---@return integer 21 | function scheduler.spawn(path, args, env) 22 | local chunk, e = kernel.loadfile(path, kernel.gen_env(kernel.syscalls)) 23 | assert(chunk, e) 24 | 25 | ---@type Thread 26 | local thread = {} 27 | thread.coroutine = coroutine.create(chunk) 28 | thread.cwd = "/" 29 | thread.comm = (args or {})[1] 30 | thread.cmdline = args or {} 31 | thread.environ = env or {} 32 | thread.fd = { 33 | kernel.filesystem.open("/dev/console", "r"), 34 | kernel.filesystem.open("/dev/console", "w"), 35 | kernel.filesystem.open("/dev/console", "w"), 36 | } 37 | 38 | scheduler.threads[#scheduler.threads + 1] = thread 39 | return #scheduler.threads 40 | end 41 | 42 | --- Fork the current thread. 43 | ---@param func function 44 | ---@return integer 45 | function kernel.syscalls.fork(func) 46 | local thread = {} 47 | 48 | for k, v in pairs(scheduler.threads[scheduler.current_pid]) do 49 | thread[k] = v 50 | end 51 | thread.coroutine = coroutine.create(func) 52 | 53 | scheduler.threads[#scheduler.threads + 1] = thread 54 | return #scheduler.threads 55 | end 56 | 57 | --- Execute a program in the current thread, replacing the current program. 58 | ---@param path string 59 | ---@param args string[] 60 | ---@param env string[] 61 | function kernel.syscalls.execve(path, args, env) 62 | local chunk, e = kernel.loadfile(path, kernel.gen_env(kernel.syscalls)) 63 | if not chunk then 64 | return nil, e 65 | end 66 | 67 | local thread = scheduler.threads[scheduler.current_pid] 68 | thread.coroutine = coroutine.create(chunk) 69 | thread.comm = (args or {})[1] 70 | thread.cmdline = args or {} 71 | thread.environ = env or {} 72 | 73 | scheduler.threads[scheduler.current_pid] = thread 74 | end 75 | 76 | --- Kill a thread. 77 | ---@param pid number 78 | ---@param signal number 79 | function scheduler.kill(pid, signal) 80 | -- TODO: Actually implement signals 81 | if scheduler.threads[pid] then 82 | if signal ~= 0 then 83 | scheduler.threads[pid] = nil 84 | end 85 | return true 86 | else 87 | return false 88 | end 89 | end 90 | kernel.syscalls.kill = scheduler.kill 91 | 92 | kernel.register_hook("timer", function() 93 | for pid, thread in pairs(scheduler.threads) do 94 | scheduler.current_pid = pid 95 | if coroutine.status(thread.coroutine) == "dead" then 96 | scheduler.kill(pid) 97 | else 98 | local ok, e = coroutine.resume(thread.coroutine, table.unpack(thread.cmdline), table.unpack(thread.environ)) 99 | if not ok then 100 | thread.fd[3]:write(debug.traceback(thread.coroutine, e) .. "\n") 101 | end 102 | end 103 | end 104 | scheduler.current_pid = 0 105 | end) 106 | 107 | kernel.scheduler = scheduler 108 | end 109 | --------------------------------------------------------------------------------