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

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

View File

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

View File

@ -1,304 +1,99 @@
local keyboard = require("keyboard") local keyboard = require("keyboard")
local shell = require("shell") local shell = require("shell")
local term = require("term") -- using term for negative scroll feature 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(...) local args = shell.parse(...)
if #args > 1 then if #args > 1 then
io.write("Usage: more <filename>\n") io.write("Usage: less <filename>\n")
io.write("- or no args reads stdin\n") io.write("- or no args reads stdin\n")
return 1 return 1
end end
local arg = args[1] or "-"
local initial_offset local cat_cmd = table.concat({"cat", ...}, " ")
if not io.output().tty then
-- test validity of args return os.execute(cat_cmd)
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
end end
local width, height = term.getViewport() term.clear()
local max_display = height - 1
-- mgr is the data manager, it keeps track of what has been loaded local preader = io.popen(cat_cmd)
-- keeps a reasonable buffer, and keeps track of file handles local buffer = setmetatable({}, {__index = function(tbl, index)
local mgr if type(index) ~= "number" or index < 1 then return end
mgr = while #tbl < index do
{ local line = preader:read()
lines = {}, -- current buffer if not line then return end
chunk, -- temp from last read line that hasn't finished wrapping table.insert(tbl, line)
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)
end end
} return tbl[index]
end})
local function update(num) local index = 1
-- unexpected
if num == 0 then local _, height = term.getViewport()
return 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 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) 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 end
if not update() then local function goforward(n)
return 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 end
while true do while true do
local ename, address, char, code, dy = term.pull() local _, y = term.getCursor()
local num local print_next_line = true
if ename == "scroll" then if y == height then
if dy < 0 then print_next_line = false
num = 3 term.clearLine()
else io.write(status)
num = -3 local _, _, _, code = term.pull("key_down")
end if code == keyboard.keys.q then
elseif ename == "key_down" then term.clearLine()
num = 0 os.exit(1) -- abort
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
elseif code == keyboard.keys["end"] then 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 end
elseif ename == "interrupted" then
break
end end
if num then if print_next_line then
update(num) local line = buffer[index]
if line then
print(line)
index = index + 1
else
term.setCursor(1, height)
end
end end
end end
term.clearLine()

View File

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

View File

@ -30,6 +30,7 @@ if #args == 0 then
if command == "exit" then if command == "exit" then
return return
elseif command ~= "" then elseif command ~= "" then
tty.window.cursor = nil -- the spawned process should use its own cursor
local result, reason = sh.execute(_ENV, command) local result, reason = sh.execute(_ENV, command)
if not result then if not result then
io.stderr:write((reason and tostring(reason) or "unknown error") .. "\n") 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) function core_cursor.vertical:echo(arg, num)
local win = tty.window local win = tty.window
local gpu = win.gpu 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 if not gpu then return end
win.nowrap = self.nowrap win.nowrap = self.nowrap
if arg == "" then -- special scroll request if arg == "" then -- special scroll request
@ -76,7 +84,7 @@ function core_cursor.vertical:echo(arg, num)
if x > width then if x > width then
win.x = ((x - 1) % width) + 1 win.x = ((x - 1) % width) + 1
win.y = y + math.floor(x / width) 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 x, y = win.x, win.y
end end
if x <= 0 or y <= 0 or y > win.height or not gpu then return 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 if not char[1] then return false end
self.blinked = true self.blinked = true
if not arg then if not arg then
io.write("\0277") out:write("\0277")
char.saved = win.saved char.saved = win.saved
gpu.setForeground(char[4] or char[2], not not char[4]) gpu.setForeground(char[4] or char[2], not not char[4])
gpu.setBackground(char[5] or char[3], not not char[5]) gpu.setBackground(char[5] or char[3], not not char[5])
end 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 elseif (arg and self.blinked) or (arg == false and char) then
self.blinked = false self.blinked = false
gpu.set(win.x + win.dx, win.y + win.dy, char[1]) gpu.set(win.x + win.dx, win.y + win.dy, char[1])
if not arg then if not arg then
win.saved = char.saved win.saved = char.saved
io.write("\0278") out:write("\0278")
char = nil char = nil
end end
end end
self.char_at_cursor = char self.char_at_cursor = char
return true return true
end end
return io.write(arg) return out:write(arg)
end end
function core_cursor.vertical:handle(name, char, code) 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 if type(to_io) == "number" then -- io to io
-- from_io and to_io should be numbers -- from_io and to_io should be numbers
ios[from_io] = ios[to_io] ios[from_io] = io.dup(ios[to_io])
else else
-- to_io should be a string -- to_io should be a string
local file, reason = io.open(shell.resolve(to_io), mode) local file, reason = io.open(shell.resolve(to_io), mode)

View File

@ -108,6 +108,19 @@ function io.write(...)
return io.output():write(...) return io.output():write(...)
end 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 return io

View File

@ -81,21 +81,18 @@ local pipe_stream =
if coroutine.status(self.next) == "suspended" then if coroutine.status(self.next) == "suspended" then
self:continue() self:continue()
end end
self.redirect = {}
end, end,
seek = function() seek = function()
return nil, "bad file descriptor" return nil, "bad file descriptor"
end, end,
write = function(self, value) 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 next is dead, ignore all writes
if coroutine.status(self.next) ~= "dead" then if coroutine.status(self.next) ~= "dead" then
io.stderr:write("attempt to use a closed stream\n") io.stderr:write("attempt to use a closed stream\n")
os.exit(1) os.exit(1)
end end
elseif self.redirect[1] then else
return self.redirect[1]:write(value)
elseif not self.closed then
self.buffer = self.buffer .. value self.buffer = self.buffer .. value
return self:continue(true) return self:continue(true)
end end
@ -105,11 +102,7 @@ local pipe_stream =
if self.closed then if self.closed then
return nil -- eof return nil -- eof
end end
if self.redirect[0] then if self.buffer == "" 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
-- the pipe_stream write resume is waiting on this process B (A|B) to yield -- 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 -- 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 -- natural yield (i.e. for events). To differentiate this yield from natural
@ -139,7 +132,7 @@ function pipe.buildPipeChain(progs)
local piped_stream local piped_stream
if i < #progs then 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) process.closeOnExit(handle, proc)
piped_stream = buffer.new("rw", handle) piped_stream = buffer.new("rw", handle)
piped_stream:setvbuf("no", 1024) piped_stream:setvbuf("no", 1024)
@ -147,7 +140,6 @@ function pipe.buildPipeChain(progs)
end end
if prev_piped_stream then if prev_piped_stream then
prev_piped_stream.stream.redirect[0] = rawget(pio, 0)
prev_piped_stream.stream.next = thread prev_piped_stream.stream.next = thread
pio[0] = prev_piped_stream pio[0] = prev_piped_stream
end end

View File

@ -97,7 +97,9 @@ function process.load(path, env, init, name)
parent = p, parent = p,
instances = setmetatable({}, {__mode="v"}), 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}) setmetatable(new_proc.data, {__index=p.data})
process.list[thread] = new_proc process.list[thread] = new_proc