Merge branch 'master-MC1.11' into master-MC1.12

This commit is contained in:
payonel 2018-09-02 12:50:24 -07:00
commit 61a6ecfaa1
9 changed files with 139 additions and 344 deletions

View File

@ -2,18 +2,17 @@ local shell = require("shell")
local fs = require("filesystem")
local args = shell.parse(...)
local ec = 0
if #args == 0 then
args = {"-"}
end
local input_method, input_param = "read", require("tty").window.width
local input_method, input_param = "read", require("tty").getViewport()
for i = 1, #args do
local arg = shell.resolve(args[i])
if fs.isDirectory(arg) then
io.stderr:write(string.format('cat %s: Is a directory\n', arg))
ec = 1
os.exit(1)
else
local file, reason
if args[i] == "-" then
@ -24,7 +23,7 @@ for i = 1, #args do
end
if not file then
io.stderr:write(string.format("cat: %s: %s\n", args[i], tostring(reason)))
ec = 1
os.exit(1)
else
repeat
local chunk = file[input_method](file, input_param)
@ -36,5 +35,3 @@ for i = 1, #args do
end
end
end
return ec

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()

View File

@ -1,7 +1,6 @@
local buffer = require("buffer")
local keyboard = require("keyboard")
local shell = require("shell")
local tty = require("tty")
local term = require("term")
local args = shell.parse(...)
if #args > 1 then
@ -10,48 +9,36 @@ if #args > 1 then
return 1
end
local function clear_line()
tty.window.x = 1 -- move cursor to start of line
io.write("\27[2K") -- clear line
local cat_cmd = table.concat({"cat", ...}, " ")
if not io.output().tty then
return os.execute(cat_cmd)
end
if io.output().tty then
io.write("\27[2J\27[H")
term.clear()
local intercept_active = true
local original_stream = io.stdout.stream
local custom_stream = setmetatable({
scroll = function(...)
local _, height, _, _, _, y = tty.getViewport()
local lines_below = height - y
if intercept_active and lines_below < 1 then
intercept_active = false
original_stream.scroll(-lines_below) -- if zero no scroll action is made [good]
tty.setCursor(1, height) -- move to end
clear_line()
io.write(":") -- status
local _, _, _, code = original_stream:pull(nil, "key_down") -- nil timeout is math.huge
if code == keyboard.keys.q then
clear_line()
os.exit(1) -- abort
elseif code == keyboard.keys["end"] then
io.stdout.stream.scroll = nil -- remove handler
elseif code == keyboard.keys.space or code == keyboard.keys.pageDown then
io.write("\27[2J\27[H") -- clear whole screen, get new page drawn; move cursor to 1,1
elseif code == keyboard.keys.enter or code == keyboard.keys.down then
clear_line() -- remove status bar
original_stream.scroll(1) -- move everything up one
tty.setCursor(1, height - 1)
end
intercept_active = true
end
return original_stream.scroll(...)
local preader = io.popen(cat_cmd)
local intercept = true
while true do
local _, height, _, _, _, y = term.getViewport()
if intercept and y == height then
term.clearLine()
io.write(":") -- status
::INPUT::
local _, _, _, code = term.pull("key_down")
if code == keyboard.keys.q then
term.clearLine()
os.exit(1) -- abort
elseif code == keyboard.keys["end"] then
intercept = false
elseif code == keyboard.keys.space or code == keyboard.keys.pageDown then
term.clear() -- clear whole screen, get new page drawn; move cursor to 1,1
elseif code == keyboard.keys.enter or code == keyboard.keys.down then
term.clearLine() -- remove status bar
term.scroll(1) -- move everything up one
term.setCursor(1, height - 1)
else
goto INPUT
end
}, {__index=original_stream})
local custom_output_buffer = buffer.new("w", custom_stream)
custom_output_buffer:setvbuf("no")
io.output(custom_output_buffer)
end
print(preader:read() or os.exit())
end
return loadfile(shell.resolve("cat", "lua"))(...)

View File

@ -30,6 +30,7 @@ if #args == 0 then
if command == "exit" then
return
elseif command ~= "" then
tty.window.cursor = nil -- the spawned process should use its own cursor
local result, reason = sh.execute(_ENV, command)
if not result then
io.stderr:write((reason and tostring(reason) or "unknown error") .. "\n")

View File

@ -69,6 +69,14 @@ end
function core_cursor.vertical:echo(arg, num)
local win = tty.window
local gpu = win.gpu
-- we should not use io.write
-- the cursor should echo to the stream it is reading from
-- this makes sense because a process may redirect its io
-- but a cursor reading from a given stdin tty should also
-- echo to that same stream
local out = io.stdin.stream
if not gpu then return end
win.nowrap = self.nowrap
if arg == "" then -- special scroll request
@ -76,7 +84,7 @@ function core_cursor.vertical:echo(arg, num)
if x > width then
win.x = ((x - 1) % width) + 1
win.y = y + math.floor(x / width)
io.write("") -- tty.stream:write knows how to scroll vertically
out:write("") -- tty.stream:write knows how to scroll vertically
x, y = win.x, win.y
end
if x <= 0 or y <= 0 or y > win.height or not gpu then return end
@ -108,25 +116,25 @@ function core_cursor.vertical:echo(arg, num)
if not char[1] then return false end
self.blinked = true
if not arg then
io.write("\0277")
out:write("\0277")
char.saved = win.saved
gpu.setForeground(char[4] or char[2], not not char[4])
gpu.setBackground(char[5] or char[3], not not char[5])
end
io.write("\0277", "\27[7m", char[1], "\0278")
out:write("\0277\27[7m"..char[1].."\0278")
elseif (arg and self.blinked) or (arg == false and char) then
self.blinked = false
gpu.set(win.x + win.dx, win.y + win.dy, char[1])
if not arg then
win.saved = char.saved
io.write("\0278")
out:write("\0278")
char = nil
end
end
self.char_at_cursor = char
return true
end
return io.write(arg)
return out:write(arg)
end
function core_cursor.vertical:handle(name, char, code)

View File

@ -85,7 +85,7 @@ function sh.internal.openCommandRedirects(redirects)
if type(to_io) == "number" then -- io to io
-- from_io and to_io should be numbers
ios[from_io] = ios[to_io]
ios[from_io] = io.dup(ios[to_io])
else
-- to_io should be a string
local file, reason = io.open(shell.resolve(to_io), mode)

View File

@ -108,6 +108,19 @@ function io.write(...)
return io.output():write(...)
end
function io.dup(fd)
return setmetatable({
close = function(self) self._closed = true end,
}, {__index = function(_, key)
local fd_value = fd[key]
if type(fd_value) ~= "function" then return fd_value end
return function(self, ...)
if self._closed then return nil, "closed stream" end
return fd_value(fd, ...)
end
end})
end
-------------------------------------------------------------------------------
return io

View File

@ -81,21 +81,18 @@ local pipe_stream =
if coroutine.status(self.next) == "suspended" then
self:continue()
end
self.redirect = {}
end,
seek = function()
return nil, "bad file descriptor"
end,
write = function(self, value)
if not self.redirect[1] and self.closed then
if self.closed then
-- if next is dead, ignore all writes
if coroutine.status(self.next) ~= "dead" then
io.stderr:write("attempt to use a closed stream\n")
os.exit(1)
end
elseif self.redirect[1] then
return self.redirect[1]:write(value)
elseif not self.closed then
else
self.buffer = self.buffer .. value
return self:continue(true)
end
@ -105,11 +102,7 @@ local pipe_stream =
if self.closed then
return nil -- eof
end
if self.redirect[0] then
-- popen could be using this code path
-- if that is the case, it is important to leave stream.buffer alone
return self.redirect[0]:read(n)
elseif self.buffer == "" then
if self.buffer == "" then
-- the pipe_stream write resume is waiting on this process B (A|B) to yield
-- yield here requests A to output again. However, B may elsewhere want a
-- natural yield (i.e. for events). To differentiate this yield from natural
@ -139,7 +132,7 @@ function pipe.buildPipeChain(progs)
local piped_stream
if i < #progs then
local handle = setmetatable({redirect = {rawget(pio, 1)},buffer = ""}, {__index = pipe_stream})
local handle = setmetatable({buffer = ""}, {__index = pipe_stream})
process.closeOnExit(handle, proc)
piped_stream = buffer.new("rw", handle)
piped_stream:setvbuf("no", 1024)
@ -147,7 +140,6 @@ function pipe.buildPipeChain(progs)
end
if prev_piped_stream then
prev_piped_stream.stream.redirect[0] = rawget(pio, 0)
prev_piped_stream.stream.next = thread
pio[0] = prev_piped_stream
end

View File

@ -97,7 +97,9 @@ function process.load(path, env, init, name)
parent = p,
instances = setmetatable({}, {__mode="v"}),
}
setmetatable(new_proc.data.io, {__index=p.data.io})
for i,fd in pairs(p.data.io) do
new_proc.data.io[i] = io.dup(fd)
end
setmetatable(new_proc.data, {__index=p.data})
process.list[thread] = new_proc