fixed less, simplified

This commit is contained in:
payonel 2018-09-01 16:49:31 -07:00
parent 1fb05ff432
commit b6bc9d0cea

View File

@ -1,304 +1,99 @@
local keyboard = require("keyboard")
local shell = require("shell")
local term = require("term") -- using term for negative scroll feature
local text = require("text")
local unicode = require("unicode")
local computer = require("computer")
local tx = require("transforms")
if not io.output().tty then
return loadfile(shell.resolve("cat", "lua"), "bt", _G)(...)
end
local args = shell.parse(...)
if #args > 1 then
io.write("Usage: more <filename>\n")
io.write("Usage: less <filename>\n")
io.write("- or no args reads stdin\n")
return 1
end
local arg = args[1] or "-"
local initial_offset
-- test validity of args
do
if arg == "-" then
if not io.stdin then
io.stderr:write("this process has no stdin\n")
return 1
end
-- stdin may not be core_stdin
initial_offset = io.stdin:seek("cur")
else
local file, reason = io.open(shell.resolve(arg))
if not file then
io.stderr:write(reason,'\n')
return 1
end
initial_offset = file:seek("cur")
file:close()
end
local cat_cmd = table.concat({"cat", ...}, " ")
if not io.output().tty then
return os.execute(cat_cmd)
end
local width, height = term.getViewport()
local max_display = height - 1
term.clear()
-- mgr is the data manager, it keeps track of what has been loaded
-- keeps a reasonable buffer, and keeps track of file handles
local mgr
mgr =
{
lines = {}, -- current buffer
chunk, -- temp from last read line that hasn't finished wrapping
lines_released = 0,
can_seek = initial_offset,
capacity = math.max(1, math.min(max_display * 10, computer.freeMemory() / 2 / width)),
size = 0,
file = nil,
path = arg ~= "-" and shell.resolve(arg) or nil,
open = function()
mgr.file = mgr.path and io.open(mgr.path) or io.stdin
end,
top_of_file = max_display,
total_lines = nil, -- nil means unknown
latest_line = nil, -- used for status improvements
rollback = function()
if not mgr.can_seek then
return false
end
if not mgr.file then
mgr.open()
elseif not mgr.file:seek("set", 0) then
mgr.close()
return false
end
mgr.lines_released = 0
mgr.lines = {}
mgr.size = 0
return true
end,
at = function(line_number)
local index = line_number - mgr.lines_released
if index < 1 then
index = index + mgr.capacity
if #mgr.lines ~= mgr.capacity or index <= mgr.size then
return nil
end
elseif index > mgr.size then
return nil
end
return mgr.lines[index] -- cached
end,
load = function(line_number)
local index = line_number - mgr.lines_released
if mgr.total_lines and mgr.total_lines < line_number then
return nil
end
if mgr.at(line_number) then
return true
end
-- lines[index] is line (lines_released + index) in the file
-- thus index == line_number - lines_released
if index <= 0 then
-- we have previously freed some of the buffer, and now the user wants it back
if not mgr.rollback() then
-- TODO how to nicely fail if can_seek == false
-- or if no more buffers
error("cannot load prior data")
end
return mgr.load(line_number) -- retry
end
if mgr.read_next() then
return mgr.load(line_number) -- retry
end
-- ran out of file, could not reach line_number
end,
write = function(line_number)
local line = mgr.at(line_number)
if not line then return false end
term.write(line)
end,
close = function()
if mgr.file then
mgr.file:close()
mgr.file = nil
end
end,
last = function()
-- return the last line_number available right now in the cache
return mgr.size + mgr.lines_released
end,
check_capacity = function(release)
-- if we have reached capacity
if mgr.size >= mgr.capacity then
if release then
mgr.lines_released = mgr.lines_released + mgr.size
mgr.size = 0
end
return true
end
end,
insert = function(line)
if mgr.check_capacity() then return false end
mgr.size = mgr.size + 1
mgr.lines[mgr.size] = line
-- latest_line is not used for computation, just for status reports
mgr.latest_line = math.max(mgr.latest_line or 0, mgr.size + mgr.lines_released)
return true
end,
read_next = function()
-- total_lines indicates we've reached the end previously
-- but have we just prior to this reached the end?
if mgr.last() == mgr.total_lines then
-- then there is no more after that point
return nil
end
if not mgr.file then
mgr.open()
end
mgr.check_capacity(true)
if not mgr.chunk then
mgr.chunk = mgr.file:read("*l")
if not mgr.chunk then
mgr.total_lines = mgr.size + mgr.lines_released -- now file length is known
mgr.close()
end
end
while mgr.chunk do
local wrapped, next = text.wrap(text.detab(mgr.chunk), width, width)
-- insert fails if capacity is full
if not mgr.insert(wrapped) then
return mgr.last()
end
mgr.chunk = next
end
return mgr.last()
end,
scroll = function(num)
if num < 0 then
num = math.max(num, mgr.top_of_file)
if num >= 0 then
return true -- nothing to scroll
end
end
term.setCursor(1, height)
local y = height
term.clearLine()
if num < 0 then
term.scroll(num) -- push text down
mgr.top_of_file = mgr.top_of_file - num
y = 1
term.setCursor(1, y) -- ready to write lines above
num = -num -- now print forward
end
local range
while num > 0 do
-- trigger load of data if needed
local line_number = y - mgr.top_of_file
if not mgr.load(line_number) then -- nothing more to read from the file
return range ~= nil -- first time it is nil
end
-- print num range of what is available, scroll to show it (if bottom of screen)
range = math.min(num, mgr.last() - line_number + 1)
if y == height then
range = math.min(range, max_display)
term.scroll(range)
y = y - range
term.setCursor(1, y)
mgr.top_of_file = mgr.top_of_file - range
end
for i=1,range do
mgr.write(line_number + i - 1)
term.setCursor(1, y + i)
end
y = y + range
num = num - range
end
return true
end,
print_status = function()
local first = mgr.top_of_file >= 1 and 1 or 1 - mgr.top_of_file
local perc = not mgr.total_lines and "--" or tostring((max_display - mgr.top_of_file) / mgr.total_lines * 100):gsub("%..*","")
local last_plus = mgr.total_lines and "" or "+"
local status = string.format("%s lines %d-%d/%s %s%%", mgr.path or "-", first, max_display - mgr.top_of_file, tostring(mgr.total_lines or mgr.latest_line)..last_plus, perc)
local gpu = term.gpu()
local sf, sb = gpu.setForeground, gpu.setBackground
local b_color, b_is_palette = gpu.getBackground()
local f_color, f_is_palette = gpu.getForeground()
sf(b_color, b_is_palette)
sb(f_color, f_is_palette)
term.write(status)
sb(b_color, b_is_palette)
sf(f_color, f_is_palette)
local preader = io.popen(cat_cmd)
local buffer = setmetatable({}, {__index = function(tbl, index)
if type(index) ~= "number" or index < 1 then return end
while #tbl < index do
local line = preader:read()
if not line then return end
table.insert(tbl, line)
end
}
return tbl[index]
end})
local function update(num)
-- unexpected
if num == 0 then
return
local index = 1
local _, height = term.getViewport()
local status = ":"
local function goback(n)
n = math.min(index - height, n)
if n <= 0 then return end -- make no back scroll, we're at the top
term.scroll(-n)
index = index - n
for y = 1, math.min(height - 1, n) do
term.setCursor(1, y)
print(buffer[index - height + y])
end
-- if this a positive direction, and we didn't previously know this was the end of the stream, give the user a once chance
local end_is_known = mgr.total_lines
-- clear buttom line, for status
local ok = mgr.scroll(num or max_display)
-- print status
term.setCursor(1, height)
-- we have to clear again in case we scrolled up
term.clearLine()
mgr.print_status()
return not end_is_known or ok
end
if not update() then
return
local function goforward(n)
if not buffer[index] then return end -- nothing to do
for test_index = index, index + n do
if not buffer[test_index] then
n = math.min(n, test_index - index)
break
end
end
term.clearLine()
term.scroll(n)
if n >= height then
index = index + (n - height) + 1
n = height - 1
end
term.setCursor(1, height - n)
return true
end
while true do
local ename, address, char, code, dy = term.pull()
local num
if ename == "scroll" then
if dy < 0 then
num = 3
else
num = -3
end
elseif ename == "key_down" then
num = 0
if code == keyboard.keys.q or code == keyboard.keys.d and keyboard.isControlDown() then
break
elseif code == keyboard.keys.space or code == keyboard.keys.pageDown then
num = max_display
elseif code == keyboard.keys.pageUp then
num = -max_display
elseif code == keyboard.keys.enter or code == keyboard.keys.down then
num = 1
elseif code == keyboard.keys.up then
num = -1
elseif code == keyboard.keys.home then
num = -math.huge
local _, y = term.getCursor()
local print_next_line = true
if y == height then
print_next_line = false
term.clearLine()
io.write(status)
local _, _, _, code = term.pull("key_down")
if code == keyboard.keys.q then
term.clearLine()
os.exit(1) -- abort
elseif code == keyboard.keys["end"] then
num = math.huge
print_next_line = goforward(math.huge)
elseif code == keyboard.keys.space or code == keyboard.keys.pageDown then
print_next_line = goforward(height - 1)
elseif code == keyboard.keys.enter or code == keyboard.keys.down then
print_next_line = goforward(1)
elseif code == keyboard.keys.up then
goback(1)
elseif code == keyboard.keys.pageUp then
goback(height - 1)
end
elseif ename == "interrupted" then
break
end
if num then
update(num)
if print_next_line then
local line = buffer[index]
if line then
print(line)
index = index + 1
else
term.setCursor(1, height)
end
end
end
term.clearLine()