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

View File

@ -101,6 +101,7 @@ function component.setPrimary(componentType, address)
if wasAvailable or wasAdding then
adding[componentType] = {
address=address,
proxy = primary,
timer=event.timer(0.1, function()
adding[componentType] = nil
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)
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)
end
end
@ -132,9 +174,31 @@ local function onComponentRemoved(_, address, componentType)
if primaries[componentType] and primaries[componentType].address == address or
adding[componentType] and adding[componentType].address == address
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
event.listen("component_added", onComponentAdded)
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
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)
screen=component.proxy(screen)
term.bind(gpu, screen)
term.bind(gpu)
computer.pushSignal("term_available")
end)
event.listen("component_unavailable", function(_,type)
if type == "screen" or type == "gpu" then
if term.isAvailable() then
local function components_changed(ename, address, type)
local window = term.internal.window()
if window[type] and not component.proxy(window[type].address) then
window[type] = nil
if not window then
return
end
if ename == "component_available" or ename == "component_unavailable" then
type = address
end
if not term.isAvailable() then
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
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)
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
end
end)

View File

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

View File

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

View File

@ -2,6 +2,7 @@ local unicode = require("unicode")
local event = require("event")
local process = require("process")
local kb = require("keyboard")
local component = require("component")
local keys = kb.keys
local term = {}
@ -51,7 +52,7 @@ end
function term.isAvailable(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
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)}
local c11,c12 = unpack(c[1])
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
sf(c11,c12)
sb(c21,c22)
@ -227,54 +233,64 @@ function term.readKeyboard(ops)
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}
input.blink = ops.blink
if input.blink == nil then input.blink = w.blink end
if ops.nowrap then
term.internal.build_horizontal_reader(input)
else
term.internal.build_vertical_reader(input)
if input.blink == nil then
input.blink = w.blink
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
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 backup_cache = hints.cache
if name =="interrupted" then draw("^C\n",true) return ""
elseif name=="touch" or name=="drag" then term.internal.onTouch(input,char,code)
elseif name=="clipboard" then c=char hints.cache = nil
elseif name=="key_down" then
if name == "interrupted" or name == "term_unavailable" then
draw("^C\n",true)
return ""
elseif address == main_kb or address == main_sc then
if name == "touch" or name == "drag" then
term.internal.onTouch(input,char,code)
elseif name == "clipboard" then
c = char
hints.cache = nil
elseif name == "key_down" then
hints.cache = nil
local ctrl = kb.isControlDown(address)
if ctrl and code == keys.d then return
elseif char==9 then hints.cache = backup_cache term.internal.tab(input,hints)
elseif char==13 and filter(input) then
input:move(math.huge)
if db ~= false then draw("\n") end
term.internal.read_history(history,input)
return input.data.."\n"
elseif code==keys.back then
input:update(-1)
elseif code==keys.left then
input:move(ctrl and term.internal.ctrl_movement(input, -1) or -1)
elseif code==keys.right then
input:move(ctrl and term.internal.ctrl_movement(input, 1) or 1)
elseif ctrl and char=="w" then
-- cut word
elseif code==keys.up then
term.internal.read_history(history,input,1)
elseif code==keys.down then
term.internal.read_history(history,input,-1)
elseif code==keys.home then
input:move(-math.huge)
elseif code==keys["end"] then
input:move(math.huge)
elseif code==keys.delete then
input:update(0)
elseif char>=32 then
c=unicode.char(char)
else
if ctrl and code == keys.d then return --nil
elseif char == 9 then
hints.cache = backup_cache
term.internal.tab(input,hints)
elseif char == 13 and filter(input) then
input:move(math.huge)
if db ~= false then
draw("\n")
end
term.internal.read_history(history,input)
return input.data .. "\n"
elseif code == keys.up then term.internal.read_history(history, input, 1)
elseif code == keys.down then term.internal.read_history(history, input, -1)
elseif code == keys.left then input:move(ctrl and term.internal.ctrl_movement(input, -1) or -1)
elseif code == keys.right then input:move(ctrl and term.internal.ctrl_movement(input, 1) or 1)
elseif code == keys.home then input:move(-math.huge)
elseif code == keys["end"] then input:move( math.huge)
elseif code == keys.back then c = -1
elseif code == keys.delete then c = 0
elseif char >= 32 then c = unicode.char(char)
elseif ctrl and char == "w"then -- TODO: cut word
else hints.cache = backup_cache -- ignored chars shouldn't clear hint cache
end
end
-- if we obtained something (c) to handle
if c then
input:update(c)
end
end
if c then input:update(c) end
end
end
@ -380,16 +396,56 @@ function term.getCursorBlink()
return W().blink
end
function term.bind(gpu, screen, kb, window)
function term.bind(gpu, window)
window = window or W()
window.gpu = gpu or window.gpu
window.screen = screen or window.screen
window.keyboard = kb or window.keyboard
window.keyboard = nil -- without a keyboard bound, always use the screen's main keyboard (1st)
if window.fullscreen then
term.setViewport(nil,nil,nil,nil,window.x,window.y,window)
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)
-- if zero scroll length is requested, do nothing
if number == 0 then return end
@ -552,7 +608,13 @@ function --[[@delayloaded-start@]] term.internal.tab(input,hints)
hints.cache.i=-1
end
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)
local next=c[c.i+1]
if next then
@ -568,21 +630,4 @@ function --[[@delayloaded-start@]] term.getGlobalArea(window)
return dx+1,dy+1,w,h
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