mirror of
https://github.com/MightyPirates/OpenComputers.git
synced 2025-09-18 03:36:47 -04:00
Cleaned up and simplified tab-completion logic a little.
Added shift-tab to inverse cycle suggestions. Starting completion from cursor location now (sh just eats everything after the cursor for now).
This commit is contained in:
parent
e3a6a3a9b4
commit
d7e7a7edf2
@ -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")
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user