diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/sh.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/sh.lua index 2bab3714c..a625e7ece 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/bin/sh.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/bin/sh.lua @@ -6,6 +6,7 @@ local process = require("process") local shell = require("shell") local term = require("term") local text = require("text") +local unicode = require("unicode") local function expand(value) local result = value:gsub("%$(%w+)", os.getenv):gsub("%$%b{}", @@ -148,74 +149,66 @@ end local args, options = shell.parse(...) local history = {} -local lastSearch - -local function drawPrompt() - local foreground = component.gpu.setForeground(0xFF0000) - term.write(expand(os.getenv("PS1") or "$ ")) - component.gpu.setForeground(foreground) -end - -local function getMatchingPrograms(pattern) - local res = {} - for dir in string.gmatch(os.getenv("PATH"), "[a-zA-Z0-9/.]+") do - for file in fs.list(dir) do - if string.match(file, "^" .. pattern .. "(.+)[.]lua") then - res[#res+1] = file:match("(.+).lua") - end - end - end - return res -end - -local function getMatchingFiles(pattern) - local res = {} - local dir = fs.isDirectory(pattern) and pattern or fs.path(pattern) or "/" - local name = (dir == pattern) and "" or fs.name(pattern) or "" - for file in fs.list(dir) do - if string.match(file, "^" .. name) then - res[#res+1] = file - end - end - return res -end - -local function hintHandler(line) - local base, space, after = string.match(line, "(.+)(%s)(.+)") - local searchProgram = not base - if not base then - base = "" - after = line or "" - end - if searchProgram then - local matches - if after:find("[/.]") == 1 then - matches = getMatchingFiles(after) - if #matches == 1 then - lastSearch = "" - local ret = base .. (space or "") .. after .. matches[1]:gsub(after:match("[/]*(%w+)$"),"",1) - return ret:gsub("[^/]$","%1 ") - end - else - matches = getMatchingPrograms(after) - if #matches == 1 then - lastSearch = "" - return matches[1] .. " " - end - end - - if lastSearch == line then return matches end +local function getMatchingPrograms(baseName) + local result = {} + -- TODO only matching files with .lua extension for now, might want to + -- extend this to other extensions at some point? env var? file attrs? + if not baseName or #baseName == 0 then + baseName = "^(.*)%.lua$" else - local matches = getMatchingFiles(after) - - for k in ipairs(matches)do - local ret = base .. space .. after .. (matches[k]):gsub(after:match("[/]*(%w+)$") or "","",1) - matches[k] = ret:gsub("[^/]$","%1 "):gsub("/$","") - end - - return matches + baseName = "^(" .. baseName .. ".*)%.lua$" end - lastSearch = line + for basePath in string.gmatch(os.getenv("PATH"), "[^:]+") do + for file in fs.list(basePath) do + local match = file:match(baseName) + if match then + table.insert(result, match) + end + end + end + return result +end + +local function getMatchingFiles(baseName) + local result, basePath = {} + -- note: we strip the trailing / to make it easier to navigate through + -- directories using tab completion (since entering the / will then serve + -- as the intention to go into the currently hinted one). + if fs.isDirectory(baseName) then + basePath = baseName + baseName = "^(.-)/?$" + else + basePath = fs.path(baseName) or "/" + baseName = "^(" .. fs.name(baseName) .. ".-)/?$" + end + for file in fs.list(basePath) do + local match = file:match(baseName) + if match then + table.insert(result, fs.concat(basePath, match)) + end + end + return result +end + +local function hintHandler(line, cursor) + local line = unicode.sub(line, 1, cursor - 1) + if not line or #line < 1 then + return nil + end + local result + local prefix, partial = string.match(line, "^(.+%s)(.+)$") + local searchInPath = not prefix and not line:find("/") + if searchInPath then + -- first part and no path, look for programs in the $PATH + result = getMatchingPrograms(line) + else -- just look normal files + result = getMatchingFiles(partial or line) + end + for i = 1, #result do + result[i] = (prefix or "") .. result[i] .. (searchInPath and " " or "") + end + table.sort(result) + return result end if #args == 0 and (io.input() == io.stdin or options.i) and not options.c then @@ -228,7 +221,9 @@ if #args == 0 and (io.input() == io.stdin or options.i) and not options.c then term.clear() end while term.isAvailable() do - drawPrompt() + local foreground = component.gpu.setForeground(0xFF0000) + term.write(expand(os.getenv("PS1") or "$ ")) + component.gpu.setForeground(foreground) local command = term.read(history, nil, hintHandler) if not command then term.write("exit\n") diff --git a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua index 27092c9e7..8428c2563 100644 --- a/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua +++ b/src/main/resources/assets/opencomputers/loot/OpenOS/lib/term.lua @@ -101,16 +101,22 @@ function term.isAvailable() return component.isAvailable("gpu") and component.isAvailable("screen") end -function term.read(history, dobreak, hint, prompt) +function term.read(history, dobreak, hint) checkArg(1, history, "table", "nil") + checkArg(3, hint, "function", "table", "nil") history = history or {} table.insert(history, "") local offset = term.getCursor() - 1 local scrollX, scrollY = 0, #history - 1 local cursorX = 1 - local hintCache = (type(hint)=="table" and #hint > 1)and hint - local selectedHint = 0 + if type(hint) == "table" then + local hintTable = hint + hint = function() + return hintTable + end + end + local hintCache, hintIndex local function getCursor() return cursorX, 1 + scrollY @@ -121,6 +127,10 @@ function term.read(history, dobreak, hint, prompt) return history[cby] end + local function clearHint() + hintCache = nil + end + local function setCursor(nbx, nby) local w, h = component.gpu.getResolution() local cx, cy = term.getCursor() @@ -149,6 +159,7 @@ function term.read(history, dobreak, hint, prompt) cursorX = nbx term.setCursor(nbx - scrollX + offset, cy) + clearHint() end local function copyIfNecessary() @@ -216,6 +227,7 @@ function term.read(history, dobreak, hint, prompt) local function delete() copyIfNecessary() + clearHint() local cbx, cby = getCursor() if cbx <= unicode.len(line()) then local cw = unicode.charWidth(unicode.sub(line(), cbx)) @@ -235,6 +247,7 @@ function term.read(history, dobreak, hint, prompt) local function insert(value) copyIfNecessary() + clearHint() local cx, cy = term.getCursor() local cbx, cby = getCursor() local w, h = component.gpu.getResolution() @@ -250,61 +263,58 @@ function term.read(history, dobreak, hint, prompt) right(unicode.len(value)) end - local function tab() - if not hintCache then - if type(hint) == "function" then - local h = hint(line()) - if type(h) == "string" then - local _, cby = getCursor() - history[cby] = after - elseif type(h) == "table" and #h > 0 then - hintCache = h - selectedHint = 1 - local _, cby = getCursor() - history[cby] = hintCache[selectedHint] or "" - end + local function tab(direction) + local cbx, cby = getCursor() + if not hintCache then -- hint is never nil, see onKeyDown + hintCache = hint(line(), cbx) + hintIndex = 0 + if type(hintCache) == "string" then + hintCache = {hintCache} + end + if type(hintCache) ~= "table" or #hintCache < 1 then + hintCache = nil -- invalid hint end - else - selectedHint = (selectedHint+1)<=#hintCache and (selectedHint+1) or 1 - local _, cby = getCursor() - history[cby] = hintCache[selectedHint] or "" end - redraw() - ende() - end - - local function cleanHint() - if type(hint) ~= "table" then - hintCache = nil + if hintCache then + hintIndex = (hintIndex + direction + #hintCache - 1) % #hintCache + 1 + history[cby] = tostring(hintCache[hintIndex]) + -- because all other cases of the cursor being moved will result + -- in the hint cache getting invalidated we do that in setCursor, + -- so we have to back it up here to restore it after moving. + local savedCache = hintCache + redraw() + ende() + if #savedCache > 1 then -- stop if only one hint exists. + hintCache = savedCache + end end end local function onKeyDown(char, code) term.setCursorBlink(false) if code == keyboard.keys.back then - if left() then delete() end cleanHint() + if left() then delete() end elseif code == keyboard.keys.delete then - delete()cleanHint() + delete() elseif code == keyboard.keys.left then left() elseif code == keyboard.keys.right then right() elseif code == keyboard.keys.home then - home()cleanHint() + home() elseif code == keyboard.keys["end"] then - ende()cleanHint() + ende() elseif code == keyboard.keys.up then up() elseif code == keyboard.keys.down then down() elseif code == keyboard.keys.tab and hint then - tab() + tab(keyboard.isShiftDown() and -1 or 1) elseif code == keyboard.keys.enter then local cbx, cby = getCursor() if cby ~= #history then -- bring entry to front history[#history] = line() table.remove(history, cby) - cleanHint() end return true, history[#history] .. "\n" elseif keyboard.isControlDown() and code == keyboard.keys.d then @@ -317,7 +327,6 @@ function term.read(history, dobreak, hint, prompt) return true, nil elseif not keyboard.isControl(char) then insert(unicode.char(char)) - cleanHint() end term.setCursorBlink(true) term.setCursorBlink(true) -- force toggle to caret