make reliable screen and keyboard selection for boot and terminals

closes #1933
This commit is contained in:
payonel 2016-07-10 14:32:11 -07:00
parent 1fdd30ae72
commit d46eed8a4a
6 changed files with 232 additions and 106 deletions

View File

@ -430,7 +430,7 @@ local function find()
setStatus("Find: " .. findText) setStatus("Find: " .. findText)
local _, address, char, code = term.pull("key_down") local _, address, char, code = term.pull("key_down")
if address == term.keyboard().address then if address == term.keyboard() then
local handler, name = getKeyBindHandler(code) local handler, name = getKeyBindHandler(code)
highlight(cbx, cby, unicode.wlen(findText), false) highlight(cbx, cby, unicode.wlen(findText), false)
if name == "newline" then if name == "newline" then
@ -557,7 +557,7 @@ getKeyBindHandler = function(code)
elseif value == "shift" then shift = true elseif value == "shift" then shift = true
else key = value end else key = value end
end end
local keyboardAddress = term.keyboard().address local keyboardAddress = term.keyboard()
if (not alt or keyboard.isAltDown(keyboardAddress)) and if (not alt or keyboard.isAltDown(keyboardAddress)) and
(not control or keyboard.isControlDown(keyboardAddress)) and (not control or keyboard.isControlDown(keyboardAddress)) and
(not shift or keyboard.isShiftDown(keyboardAddress)) and (not shift or keyboard.isShiftDown(keyboardAddress)) and
@ -656,7 +656,7 @@ end
while running do while running do
local event, address, arg1, arg2, arg3 = term.pull() local event, address, arg1, arg2, arg3 = term.pull()
if address == term.keyboard().address or address == term.screen().address then if address == term.keyboard() or address == term.screen() then
local blink = true local blink = true
if event == "key_down" then if event == "key_down" then
onKeyDown(arg1, arg2) onKeyDown(arg1, arg2)

View File

@ -101,6 +101,7 @@ function component.setPrimary(componentType, address)
if wasAvailable or wasAdding then if wasAvailable or wasAdding then
adding[componentType] = { adding[componentType] = {
address=address, address=address,
proxy = primary,
timer=event.timer(0.1, function() timer=event.timer(0.1, function()
adding[componentType] = nil adding[componentType] = nil
primaries[componentType] = primary primaries[componentType] = primary
@ -116,14 +117,55 @@ end
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
for address in component.list('screen', true) do
if #component.invoke(address,'getKeyboards') > 0 then
component.setPrimary('screen',address)
end
end
local function onComponentAdded(_, address, componentType) local function onComponentAdded(_, address, componentType)
if not (primaries[componentType] or adding[componentType]) then local prev = primaries[componentType] or (adding[componentType] and adding[componentType].proxy)
if prev then
-- special handlers -- some components are just better at being primary
if componentType == "screen" then
--the primary has no keyboards but we do
if #prev.getKeyboards() == 0 then
local first_kb = component.invoke(address, 'getKeyboards')[1]
if first_kb then
-- just in case our kb failed to achieve primary
-- possible if existing primary keyboard became primary first without a screen
-- then prev (a screen) was added without a keyboard
-- and then we attached this screen+kb pair, and our kb fired first - failing to achieve primary
-- also, our kb may fire right after this, which is fine
component.setPrimary("keyboard", first_kb)
prev = nil -- nil meaning we should take this new one over the previous
end
end
elseif componentType == "keyboard" then
-- to reduce signal noise, if this kb is also the prev, we do not need to reset primary
if address ~= prev.address then
--keyboards never replace primary keyboards unless the are the only keyboard on the primary screen
local current_screen = primaries.screen or (adding.screen and adding.screen.proxy)
--if there is not yet a screen, do not use this keyboard, it's not any better
if current_screen then
-- the next phase is complicated
-- there is already a screen and there is already a keyboard
-- this keyboard is only better if this is a keyboard of the primary screen AND the current keyboard is not
-- i don't think we can trust kb order (1st vs 2nd), 2nd could fire first
-- but if there are two kbs on a screen, we can give preferred treatment to the first
-- thus, assume 2nd is not attached for the purposes of primary kb
-- and THUS, whichever (if either) is the 1st kb of the current screen
-- this is only possible if
-- 1. the only kb on the system (current) has no screen
-- 2. a screen is added without a kb
-- 3. this kb is added later manually
-- prev is true when addr is not equal to the primary keyboard of the current screen -- meaning
-- when addr is different, and thus it is not the primary keyboard, then we ignore this
-- keyboard, and keep the previous
-- prev is false means we should take this new keyboard
prev = address ~= current_screen.getKeyboards()[1]
end
end
end
end
if not prev then
component.setPrimary(componentType, address) component.setPrimary(componentType, address)
end end
end end
@ -132,9 +174,31 @@ local function onComponentRemoved(_, address, componentType)
if primaries[componentType] and primaries[componentType].address == address or if primaries[componentType] and primaries[componentType].address == address or
adding[componentType] and adding[componentType].address == address adding[componentType] and adding[componentType].address == address
then then
component.setPrimary(componentType, component.list(componentType, true)()) local next = component.list(componentType, true)()
component.setPrimary(componentType, next)
if componentType == "screen" and next then
-- setPrimary already set the proxy (if successful)
local proxy = (primaries.screen or (adding.screen and adding.screen.proxy))
if proxy then
-- if a screen is removed, and the primary keyboard is actually attached to another, non-primary, screen
-- then the `next` screen, if it has a keyboard, should TAKE priority
local next_kb = proxy.getKeyboards()[1] -- costly, don't call this method often
local old_kb = primaries.keyboard or adding.keyboard
-- if the next screen doesn't have a kb, this operation is without purpose, leave things as they are
-- if there was no previous kb, use the new one
if next_kb and (not old_kb or old_kb.address ~= next_kb) then
component.setPrimary("keyboard", next_kb)
end
end
end
end end
end end
event.listen("component_added", onComponentAdded) event.listen("component_added", onComponentAdded)
event.listen("component_removed", onComponentRemoved) event.listen("component_removed", onComponentRemoved)
if _G.boot_screen then
component.setPrimary("screen", _G.boot_screen)
end
_G.boot_screen = nil

View File

@ -7,30 +7,59 @@ local process = require("process")
-- this should be the init level process -- this should be the init level process
process.info().data.window = term.internal.open() process.info().data.window = term.internal.open()
event.listen("gpu_bound", function(ename, gpu, screen) event.listen("gpu_bound", function(ename, gpu)
gpu=component.proxy(gpu) gpu=component.proxy(gpu)
screen=component.proxy(screen) term.bind(gpu)
term.bind(gpu, screen)
computer.pushSignal("term_available") computer.pushSignal("term_available")
end) end)
event.listen("component_unavailable", function(_,type) local function components_changed(ename, address, type)
if type == "screen" or type == "gpu" then local window = term.internal.window()
if term.isAvailable() then if not window then
local window = term.internal.window() return
if window[type] and not component.proxy(window[type].address) then
window[type] = nil
end
end
if not term.isAvailable() then
computer.pushSignal("term_unavailable")
end
end end
end)
if ename == "component_available" or ename == "component_unavailable" then
type = address
end
if ename == "component_removed" or ename == "component_unavailable" then
-- address can be type, when ename is *_unavailable, but *_removed works here and that's all we need
if type == "gpu" and window.gpu.address == address then
window.gpu = nil
window.keyboard = nil
elseif type == "keyboard" then
-- we could check if this was our keyboard
-- i.e. if address == window.keyboard
-- but it is also simple for the terminal to
-- recheck what kb to use
window.keyboard = nil
end
elseif (ename == "component_added" or ename == "component_available") and type == "keyboard" then
-- we need to clear the current terminals cached keyboard (if any) when
-- a new keyboard becomes available. This is in case the new keyboard was
-- attached to the terminal's window. The terminal library has the code to
-- determine what the best keyboard to use is, but here we'll just set the
-- cache to nil to force term library to reload it. An alternative to this
-- method would be to make sure the terminal library doesn't cache the
-- wrong keybaord to begin with but, users may actually expect that any
-- primary keyboard is a valid keyboard (weird, in my opinion)
window.keyboard = nil
end
if (type == "screen" or type == "gpu") and not term.isAvailable() then
computer.pushSignal("term_unavailable")
end
end
event.listen("component_removed", components_changed)
event.listen("component_added", components_changed)
event.listen("component_available", components_changed)
event.listen("component_unavailable", components_changed)
event.listen("screen_resized", function(_,addr,w,h) event.listen("screen_resized", function(_,addr,w,h)
local window = term.internal.window() local window = term.internal.window()
if term.isAvailable(window) and window.screen.address == addr and window.fullscreen then if term.isAvailable(window) and term.screen(window) == addr and window.fullscreen then
window.w,window.h = w,h window.w,window.h = w,h
end end
end) end)

View File

@ -32,9 +32,12 @@ do
for address in component.list('screen', true) do for address in component.list('screen', true) do
if #component.invoke(address, 'getKeyboards') > 0 then if #component.invoke(address, 'getKeyboards') > 0 then
screen = address screen = address
break
end end
end end
_G.boot_screen = screen
-- Report boot progress if possible. -- Report boot progress if possible.
local gpu = component.list("gpu", true)() local gpu = component.list("gpu", true)()
local w, h local w, h
@ -150,17 +153,9 @@ do
status("Initializing components...") status("Initializing components...")
local primaries = {}
for c, t in component.list() do for c, t in component.list() do
local s = component.slot(c)
if not primaries[t] or (s >= 0 and s < primaries[t].slot) then
primaries[t] = {address=c, slot=s}
end
computer.pushSignal("component_added", c, t) computer.pushSignal("component_added", c, t)
end end
for t, c in pairs(primaries) do
component.setPrimary(t, c.address)
end
os.sleep(0.5) -- Allow signal processing by libraries. os.sleep(0.5) -- Allow signal processing by libraries.
computer.pushSignal("init") -- so libs know components are initialized. computer.pushSignal("init") -- so libs know components are initialized.

View File

@ -42,14 +42,7 @@ setmetatable(keyboard.keys,
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
local function getKeyboardAddress(address) local function getKeyboardAddress(address)
if address then return address or require("term").keyboard()
return address
else
local primary = component.isAvailable("keyboard") and component.getPrimary("keyboard")
if primary then
return primary.address
end
end
end end
local function getPressedCodes(address) local function getPressedCodes(address)

View File

@ -2,6 +2,7 @@ local unicode = require("unicode")
local event = require("event") local event = require("event")
local process = require("process") local process = require("process")
local kb = require("keyboard") local kb = require("keyboard")
local component = require("component")
local keys = kb.keys local keys = kb.keys
local term = {} local term = {}
@ -51,7 +52,7 @@ end
function term.isAvailable(w) function term.isAvailable(w)
w = w or W() w = w or W()
return w and not not (w.gpu and w.screen) return w and not not (w.gpu and w.gpu.getScreen())
end end
function term.internal.pull(input, c, off, t, ...) function term.internal.pull(input, c, off, t, ...)
@ -74,6 +75,11 @@ function term.internal.pull(input, c, off, t, ...)
c=c or {{gpu.getBackground()},{gpu.getForeground()},gpu.get(x,y)} c=c or {{gpu.getBackground()},{gpu.getForeground()},gpu.get(x,y)}
local c11,c12 = unpack(c[1]) local c11,c12 = unpack(c[1])
local c21,c22 = unpack(c[2]) local c21,c22 = unpack(c[2])
-- c can fail if gpu does not have a screen
-- if can happen during a type of race condition when a screen is removed
if not c11 then
return nil, "interrupted"
end
if not off then if not off then
sf(c11,c12) sf(c11,c12)
sb(c21,c22) sb(c21,c22)
@ -227,54 +233,64 @@ function term.readKeyboard(ops)
local draw=io.stdin.tty and term.drawText or term.internal.nop local draw=io.stdin.tty and term.drawText or term.internal.nop
local input={w=w,promptx=w.x,prompty=w.y,index=0,data="",mask=pwchar} local input={w=w,promptx=w.x,prompty=w.y,index=0,data="",mask=pwchar}
input.blink = ops.blink input.blink = ops.blink
if input.blink == nil then input.blink = w.blink end if input.blink == nil then
if ops.nowrap then input.blink = w.blink
term.internal.build_horizontal_reader(input)
else
term.internal.build_vertical_reader(input)
end end
-- two wrap types currently supported, vertical and hortizontal
if ops.nowrap then term.internal.build_horizontal_reader(input)
else term.internal.build_vertical_reader(input)
end
while true do while true do
local killed, name, address, char, code = term.internal.pull(input) local killed, name, address, char, code = term.internal.pull(input)
-- we have to keep checking what kb is active in case it is switching during use
-- we could have multiple screens, each with keyboards active
local main_kb = term.keyboard(w)
local main_sc = term.screen(w)
local c = nil local c = nil
local backup_cache = hints.cache local backup_cache = hints.cache
if name =="interrupted" then draw("^C\n",true) return "" if name == "interrupted" or name == "term_unavailable" then
elseif name=="touch" or name=="drag" then term.internal.onTouch(input,char,code) draw("^C\n",true)
elseif name=="clipboard" then c=char hints.cache = nil return ""
elseif name=="key_down" then elseif address == main_kb or address == main_sc then
hints.cache = nil if name == "touch" or name == "drag" then
local ctrl = kb.isControlDown(address) term.internal.onTouch(input,char,code)
if ctrl and code == keys.d then return elseif name == "clipboard" then
elseif char==9 then hints.cache = backup_cache term.internal.tab(input,hints) c = char
elseif char==13 and filter(input) then hints.cache = nil
input:move(math.huge) elseif name == "key_down" then
if db ~= false then draw("\n") end hints.cache = nil
term.internal.read_history(history,input) local ctrl = kb.isControlDown(address)
return input.data.."\n" if ctrl and code == keys.d then return --nil
elseif code==keys.back then elseif char == 9 then
input:update(-1) hints.cache = backup_cache
elseif code==keys.left then term.internal.tab(input,hints)
input:move(ctrl and term.internal.ctrl_movement(input, -1) or -1) elseif char == 13 and filter(input) then
elseif code==keys.right then input:move(math.huge)
input:move(ctrl and term.internal.ctrl_movement(input, 1) or 1) if db ~= false then
elseif ctrl and char=="w" then draw("\n")
-- cut word end
elseif code==keys.up then term.internal.read_history(history,input)
term.internal.read_history(history,input,1) return input.data .. "\n"
elseif code==keys.down then elseif code == keys.up then term.internal.read_history(history, input, 1)
term.internal.read_history(history,input,-1) elseif code == keys.down then term.internal.read_history(history, input, -1)
elseif code==keys.home then elseif code == keys.left then input:move(ctrl and term.internal.ctrl_movement(input, -1) or -1)
input:move(-math.huge) elseif code == keys.right then input:move(ctrl and term.internal.ctrl_movement(input, 1) or 1)
elseif code==keys["end"] then elseif code == keys.home then input:move(-math.huge)
input:move(math.huge) elseif code == keys["end"] then input:move( math.huge)
elseif code==keys.delete then elseif code == keys.back then c = -1
input:update(0) elseif code == keys.delete then c = 0
elseif char>=32 then elseif char >= 32 then c = unicode.char(char)
c=unicode.char(char) elseif ctrl and char == "w"then -- TODO: cut word
else else hints.cache = backup_cache -- ignored chars shouldn't clear hint cache
hints.cache = backup_cache end
end
-- if we obtained something (c) to handle
if c then
input:update(c)
end end
end end
if c then input:update(c) end
end end
end end
@ -380,16 +396,56 @@ function term.getCursorBlink()
return W().blink return W().blink
end end
function term.bind(gpu, screen, kb, window) function term.bind(gpu, window)
window = window or W() window = window or W()
window.gpu = gpu or window.gpu window.gpu = gpu or window.gpu
window.screen = screen or window.screen window.keyboard = nil -- without a keyboard bound, always use the screen's main keyboard (1st)
window.keyboard = kb or window.keyboard
if window.fullscreen then if window.fullscreen then
term.setViewport(nil,nil,nil,nil,window.x,window.y,window) term.setViewport(nil,nil,nil,nil,window.x,window.y,window)
end end
end end
function term.keyboard(window)
window = window or W() or {} -- this method needs to be safe even if there is no terminal window (e.g. no gpu)
if window.keyboard then
return window.keyboard
end
local system_keyboard = component.isAvailable("keyboard") and component.keyboard
system_keyboard = system_keyboard and system_keyboard.address or "no_system_keyboard"
local screen = term.screen(window)
if not screen then
-- no screen, no known keyboard, use system primary keyboard if any
return system_keyboard
end
-- if we are using a gpu bound to the primary scren, then use the primary keyboard
if component.isAvailable("screen") and component.screen.address == screen then
window.keyboard = system_keyboard
else
-- calling getKeyboards() on the screen is costly (time)
-- custom terminals should avoid designs that require
-- this on every key hit
-- this is expensive (slow!)
window.keyboard = component.invoke(screen, "getKeyboards")[1] or system_keyboard
end
return window.keyboard
end
function term.screen(window)
window = window or W()
local gpu = window.gpu
if not gpu then
return nil
end
return gpu.getScreen()
end
function --[[@delayloaded-start@]] term.scroll(number, window) function --[[@delayloaded-start@]] term.scroll(number, window)
-- if zero scroll length is requested, do nothing -- if zero scroll length is requested, do nothing
if number == 0 then return end if number == 0 then return end
@ -552,7 +608,13 @@ function --[[@delayloaded-start@]] term.internal.tab(input,hints)
hints.cache.i=-1 hints.cache.i=-1
end end
local c=hints.cache local c=hints.cache
local change = kb.isShiftDown(term.keyboard().address) and -1 or 1 local main_kb = term.keyboard()
-- term may not have a keyboard
-- in which case, we shouldn't be handling tab events
if not main_kb then
return
end
local change = kb.isShiftDown(main_kb) and -1 or 1
c.i=(c.i+change)%math.max(#c,1) c.i=(c.i+change)%math.max(#c,1)
local next=c[c.i+1] local next=c[c.i+1]
if next then if next then
@ -568,21 +630,4 @@ function --[[@delayloaded-start@]] term.getGlobalArea(window)
return dx+1,dy+1,w,h return dx+1,dy+1,w,h
end --[[@delayloaded-end@]] end --[[@delayloaded-end@]]
function --[[@delayloaded-start@]] term.screen(window)
return (window or W()).screen
end --[[@delayloaded-end@]]
function --[[@delayloaded-start@]] term.keyboard(window)
window = window or W()
local kba = window.keyboard and window.keyboard.address
if kba and kb.pressedCodes[kba] then return window.keyboard end
window.keyboard=nil
local component = require("component")
if not window.screen or not component.proxy(window.screen.address) then window.screen = nil return end
local kba = window.screen.getKeyboards()[1]
if not kba then return end
window.keyboard = component.proxy(kba)
return window.keyboard
end --[[@delayloaded-end@]]
return term, local_env return term, local_env