37 Commits

Author SHA1 Message Date
kepler155c@gmail.com
5933f8c40f can now use named colors 2020-04-21 22:32:12 -06:00
kepler155c@gmail.com
e703c7f7b6 changes for deprecated ui methods 2020-04-17 20:40:34 -06:00
kepler155c@gmail.com
9d2a76f4ea color rework + cleanup 2020-04-16 23:13:19 -06:00
kepler155c@gmail.com
3e41996b9b cleanup + theme editor 2020-04-14 14:12:04 -06:00
kepler155c@gmail.com
9eeec8719c cleanup 2020-04-12 18:46:26 -06:00
kepler155c@gmail.com
775871c548 mouse triple click + textEntry scroll ind 2020-04-10 22:51:18 -06:00
kepler155c@gmail.com
cd6ef0da50 use layout() where appropriate and cleanup 2020-04-09 16:08:54 -06:00
kepler155c@gmail.com
8fe6e0806c refactor + new transitions 2020-04-06 00:12:46 -06:00
kepler155c@gmail.com
7659b81d49 more editor work 2020-04-04 20:56:53 -06:00
kepler155c@gmail.com
cd58ecd861 minor tweaks 2020-04-03 18:57:57 -06:00
kepler155c@gmail.com
d88ef00652 bugfixes + tweaks for editor 2.0 2020-04-02 21:28:42 -06:00
kepler155c@gmail.com
fc1a308193 list mode for overview 2020-03-31 23:43:45 -06:00
kepler155c@gmail.com
3313fb986c minor tweaks 2020-03-31 17:23:12 -06:00
kepler155c@gmail.com
613212e751 Merge remote-tracking branch 'origin/develop-1.8' into ui-enhancements-2.0 2020-03-31 09:59:15 -06:00
kepler155c@gmail.com
5a874c1944 canvas overhaul 2020-03-31 09:57:23 -06:00
Anavrins
39522ee5b1 Cleanup 2020-03-30 02:07:20 -04:00
Anavrins
369070e19c Merge pull request #30 from Wojbie/Git-fix
Update git.lua to use headers authorization.
2020-02-09 21:35:52 -05:00
Wojbie
210c5f5a11 Update git.lua to use headers authorization. 2020-02-09 23:20:03 +01:00
Anavrins
942f0bda92 GPS overhaul
Changes how GPS works to avoid returning ambiguous coordinates and nan errors
2019-12-27 01:11:36 -05:00
Anavrins
cc80e08407 gps.lua: Added nil and nan checks 2019-12-11 11:38:12 -05:00
Anavrins
9e3cf50ccc socket.lua: Option to pass user generated keypairs 2019-12-11 11:20:12 -05:00
Anavrins
6204c46cc4 Fix empty TextEntry in cloud config 2019-12-11 11:18:02 -05:00
kepler155c@gmail.com
db2a28f04c Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2019-12-07 12:05:03 -07:00
kepler155c@gmail.com
28b2ba3386 tweaks 2019-12-07 12:04:58 -07:00
Anavrins
31f43067bf Change how Sniff.lua detects modem 2019-12-06 20:14:18 -05:00
kepler155c@gmail.com
424fff7842 apparently read was added to fs.open "r" mode in 2017 2019-12-05 13:44:35 -07:00
kepler155c@gmail.com
1e675a2e35 revert multiple sub-canvas mouse click 2019-12-01 13:25:26 -07:00
kepler155c@gmail.com
ffa412c59d ui fixes 2019-11-18 14:32:10 -07:00
kepler155c@gmail.com
a3a8c64be8 UI docs 2019-11-16 22:12:02 -07:00
kepler155c@gmail.com
efa1a5bbf5 clipping for transistions + tab ordering via index + more examples 2019-11-15 12:51:44 -07:00
kepler155c@gmail.com
14057c2bf9 moar ui examples 2019-11-14 15:43:20 -07:00
kepler155c@gmail.com
3241326a2f accelerators for any event + more inspect examples 2019-11-13 21:50:00 -07:00
kepler155c@gmail.com
db48031c7c funnel all events through emit 2019-11-13 15:17:23 -07:00
kepler155c@gmail.com
0a828fecc5 oops 2019-11-13 14:38:24 -07:00
kepler155c@gmail.com
65c6ebf711 properly handle empty text entry fields (including transformations) 2019-11-13 14:24:43 -07:00
kepler155c@gmail.com
053003f429 inspect cleanup 2019-11-12 23:04:31 -07:00
kepler155c@gmail.com
25405f15c8 UI inspector 2019-11-12 21:13:17 -07:00
103 changed files with 3681 additions and 2217 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/ignore /ignore
.project

View File

@@ -82,16 +82,60 @@ local Browser = UI.Page {
}, },
sortColumn = 'name', sortColumn = 'name',
y = 2, ey = -2, y = 2, ey = -2,
sortCompare = function(self, a, b)
if self.sortColumn == 'fsize' then
return a.size < b.size
elseif self.sortColumn == 'flags' then
return a.flags < b.flags
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end,
getRowTextColor = function(_, file)
if file.marked then
return colors.green
end
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end,
eventHandler = function(self, event)
if event.type == 'copy' then -- let copy be handled by parent
return false
end
return UI.ScrollingGrid.eventHandler(self, event)
end
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar {
columns = { columns = {
{ key = 'status' }, { key = 'status' },
{ key = 'totalSize', width = 6 }, { key = 'totalSize', width = 6 },
}, },
draw = function(self)
if self.parent.dir then
local info = '#:' .. Util.size(self.parent.dir.files)
local numMarked = Util.size(marked)
if numMarked > 0 then
info = info .. ' M:' .. numMarked
end
self:setValue('info', info)
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
UI.StatusBar.draw(self)
end
end,
},
question = UI.Question {
y = -2, x = -19,
label = 'Delete',
}, },
notification = UI.Notification { }, notification = UI.Notification { },
associations = UI.SlideOut { associations = UI.SlideOut {
backgroundColor = colors.cyan,
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = 'Save', event = 'save' }, { text = 'Save', event = 'save' },
@@ -99,7 +143,7 @@ local Browser = UI.Page {
}, },
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
x = 2, ex = -6, y = 3, ey = -5, x = 2, ex = -6, y = 3, ey = -8,
columns = { columns = {
{ heading = 'Extension', key = 'name' }, { heading = 'Extension', key = 'name' },
{ heading = 'Program', key = 'value' }, { heading = 'Program', key = 'value' },
@@ -114,8 +158,11 @@ local Browser = UI.Page {
x = -4, y = 6, x = -4, y = 6,
text = '-', event = 'remove_entry', help = 'Remove', text = '-', event = 'remove_entry', help = 'Remove',
}, },
[1] = UI.Window {
x = 2, y = -6, ex = -6, ey = -3,
},
form = UI.Form { form = UI.Form {
x = 3, y = -3, ey = -2, x = 3, y = -5, ex = -7, ey = -3,
margin = 1, margin = 1,
manualControls = true, manualControls = true,
[1] = UI.TextEntry { [1] = UI.TextEntry {
@@ -137,9 +184,7 @@ local Browser = UI.Page {
text = 'Add', event = 'add_association', text = 'Add', event = 'add_association',
}, },
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar { },
backgroundColor = colors.cyan,
},
}, },
accelerators = { accelerators = {
[ 'control-q' ] = 'quit', [ 'control-q' ] = 'quit',
@@ -175,51 +220,6 @@ function Browser.menuBar:getActive(menuItem)
return true return true
end end
function Browser.grid:sortCompare(a, b)
if self.sortColumn == 'fsize' then
return a.size < b.size
elseif self.sortColumn == 'flags' then
return a.flags < b.flags
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end
function Browser.grid:getRowTextColor(file)
if file.marked then
return colors.green
end
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end
function Browser.grid:eventHandler(event)
if event.type == 'copy' then -- let copy be handled by parent
return false
end
return UI.ScrollingGrid.eventHandler(self, event)
end
function Browser.statusBar:draw()
if self.parent.dir then
local info = '#:' .. Util.size(self.parent.dir.files)
local numMarked = Util.size(marked)
if numMarked > 0 then
info = info .. ' M:' .. numMarked
end
self:setValue('info', info)
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
UI.StatusBar.draw(self)
end
end
function Browser:setStatus(status, ...) function Browser:setStatus(status, ...)
self.notification:info(string.format(status, ...)) self.notification:info(string.format(status, ...))
end end
@@ -255,7 +255,6 @@ function Browser:getDirectory(directory)
end end
function Browser:updateDirectory(dir) function Browser:updateDirectory(dir)
dir.size = 0 dir.size = 0
dir.totalSize = 0 dir.totalSize = 0
Util.clear(dir.files) Util.clear(dir.files)
@@ -344,7 +343,7 @@ function Browser:eventHandler(event)
local file = self.grid:getSelected() local file = self.grid:getSelected()
if event.type == 'quit' then if event.type == 'quit' then
Event.exitPullEvents() UI:quit()
elseif event.type == 'edit' and file then elseif event.type == 'edit' and file then
self:run('edit', file.name) self:run('edit', file.name)
@@ -432,28 +431,25 @@ function Browser:eventHandler(event)
elseif event.type == 'delete' then elseif event.type == 'delete' then
if self:hasMarked() then if self:hasMarked() then
local width = self.statusBar:getColumnWidth('status') self.question:show()
self.statusBar:setColumnWidth('status', UI.term.width)
self.statusBar:setValue('status', 'Delete marked? (y/n)')
self.statusBar:draw()
self.statusBar:sync()
local _, ch = os.pullEvent('char')
if ch == 'y' or ch == 'Y' then
for _,m in pairs(marked) do
pcall(function()
fs.delete(m.fullName)
end)
end
end
marked = { }
self.statusBar:setColumnWidth('status', width)
self.statusBar:setValue('status', '/' .. self.dir.name)
self:updateDirectory(self.dir)
self.statusBar:draw()
self.grid:draw()
self:setFocus(self.grid)
end end
return true
elseif event.type == 'question_yes' then
for _,m in pairs(marked) do
pcall(fs.delete, m.fullName)
end
marked = { }
self:updateDirectory(self.dir)
self.question:hide()
self.statusBar:draw()
self.grid:draw()
self:setFocus(self.grid)
elseif event.type == 'question_no' then
self.question:hide()
self:setFocus(self.grid)
elseif event.type == 'copy' or event.type == 'cut' then elseif event.type == 'copy' or event.type == 'cut' then
if self:hasMarked() then if self:hasMarked() then
@@ -549,6 +545,4 @@ local args = Util.parse(...)
Browser:setDir(args[1] or shell.dir()) Browser:setDir(args[1] or shell.dir())
UI:setPage(Browser) UI:setPage(Browser)
UI:start()
Event.pullEvents()
UI.term:reset()

View File

@@ -1,27 +1,23 @@
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local help = _G.help local help = _G.help
UI:configure('Help', ...) UI:configure('Help', ...)
local topics = { } local topics = { }
for _,topic in pairs(help.topics()) do for _,topic in pairs(help.topics()) do
if help.lookup(topic) then table.insert(topics, { name = topic, lname = topic:lower() })
table.insert(topics, { name = topic })
end
end end
local page = UI.Page { UI:addPage('main', UI.Page {
labelText = UI.Text { UI.Text {
x = 3, y = 2, x = 3, y = 2,
value = 'Search', value = 'Search',
}, },
filter = UI.TextEntry { UI.TextEntry {
x = 10, y = 2, ex = -3, x = 10, y = 2, ex = -3,
limit = 32, limit = 32,
transform = 'lowercase',
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 4, y = 4,
@@ -29,76 +25,71 @@ local page = UI.Page {
columns = { columns = {
{ heading = 'Topic', key = 'name' }, { heading = 'Topic', key = 'name' },
}, },
sortColumn = 'name', sortColumn = 'lname',
}, },
accelerators = { accelerators = {
[ 'control-q' ] = 'quit', [ 'control-q' ] = 'quit',
enter = 'grid_select', enter = 'grid_select',
}, },
} eventHandler = function(self, event)
if event.type == 'quit' then
UI:quit()
local topicPage = UI.Page { elseif event.type == 'grid_select' then
backgroundColor = colors.black, if self.grid:getSelected() then
UI:setPage('topic', self.grid:getSelected().name)
end
elseif event.type == 'text_change' then
if not event.text then
self.grid.values = topics
else
self.grid.values = { }
for _,f in pairs(topics) do
if string.find(f.lname, event.text:lower()) then
table.insert(self.grid.values, f)
end
end
end
self.grid:update()
self.grid:setIndex(1)
self.grid:draw()
else
return UI.Page.eventHandler(self, event)
end
end,
})
UI:addPage('topic', UI.Page {
backgroundColor = 'black',
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'text', title = 'text',
event = 'back', event = 'back',
}, },
helpText = UI.TextArea { helpText = UI.TextArea {
backgroundColor = colors.black,
x = 2, ex = -1, y = 3, ey = -2, x = 2, ex = -1, y = 3, ey = -2,
}, },
accelerators = { accelerators = {
[ 'control-q' ] = 'back', [ 'control-q' ] = 'back',
backspace = 'back', backspace = 'back',
}, },
} enable = function(self, name)
local f = help.lookup(name)
function topicPage:enable(name) self.titleBar.title = name
local f = help.lookup(name) self.helpText:setText(f and Util.readFile(f) or 'No help available for ' .. name)
self.titleBar.title = name return UI.Page.enable(self)
self.helpText:setText(f and Util.readFile(f) or 'No help available for ' .. name) end,
eventHandler = function(self, event)
return UI.Page.enable(self) if event.type == 'back' then
end UI:setPage('main')
function topicPage:eventHandler(event)
if event.type == 'back' then
UI:setPage(page)
end
return UI.Page.eventHandler(self, event)
end
function page:eventHandler(event)
if event.type == 'quit' then
UI:exitPullEvents()
elseif event.type == 'grid_select' then
if self.grid:getSelected() then
local name = self.grid:getSelected().name
UI:setPage(topicPage, name)
end end
elseif event.type == 'text_change' then
if #event.text == 0 then
self.grid.values = topics
else
self.grid.values = { }
for _,f in pairs(topics) do
if string.find(f.name, event.text) then
table.insert(self.grid.values, f)
end
end
end
self.grid:update()
self.grid:setIndex(1)
self.grid:draw()
else
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
end end,
end })
local args = Util.parse(...) local args = Util.parse(...)
UI:setPage(args[1] and topicPage or page, args[1]) UI:setPage(args[1] and 'topic' or 'main', args[1])
UI:pullEvents() UI:start()

View File

@@ -58,11 +58,16 @@ local page = UI.Page {
}, },
[2] = UI.Tab { [2] = UI.Tab {
tabTitle = 'Output', tabTitle = 'Output',
backgroundColor = 'black',
output = UI.Embedded { output = UI.Embedded {
visible = true, y = 2,
maxScroll = 1000, maxScroll = 1000,
backgroundColor = colors.black, backgroundColor = 'black',
}, },
draw = function(self)
self:write(1, 1, string.rep('\131', self.width), 'black', 'primary')
self:drawChildren()
end,
}, },
}, },
} }
@@ -153,10 +158,11 @@ function page:eventHandler(event)
self.tabs:selectTab(self.tabs[2]) self.tabs:selectTab(self.tabs[2])
elseif event.type == 'autocomplete' then elseif event.type == 'autocomplete' then
local sz = #self.prompt.value local value = self.prompt.value or ''
local sz = #value
local pos = self.prompt.entry.pos local pos = self.prompt.entry.pos
self:setPrompt(autocomplete(sandboxEnv, self.prompt.value, self.prompt.entry.pos)) self:setPrompt(autocomplete(sandboxEnv, value, self.prompt.entry.pos))
self.prompt:setPosition(pos + #self.prompt.value - sz) self.prompt:setPosition(pos + #(self.prompt.value or '') - sz)
self.prompt:updateCursor() self.prompt:updateCursor()
elseif event.type == 'device' then elseif event.type == 'device' then
@@ -177,7 +183,7 @@ function page:eventHandler(event)
history:reset() history:reset()
elseif event.type == 'command_enter' then elseif event.type == 'command_enter' then
local s = tostring(self.prompt.value) local s = tostring(self.prompt.value or '')
if #s > 0 then if #s > 0 then
self:executeStatement(s) self:executeStatement(s)
@@ -195,7 +201,6 @@ function page:eventHandler(event)
command = nil command = nil
self.grid:setValues(t) self.grid:setValues(t)
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw() self:draw()
end end
return true return true
@@ -242,7 +247,6 @@ function page:setResult(result)
end end
self.grid:setValues(t) self.grid:setValues(t)
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw() self:draw()
end end
@@ -372,7 +376,7 @@ function page:executeStatement(statement)
end end
if _exit then if _exit then
UI:exitPullEvents() UI:quit()
end end
end end
@@ -381,7 +385,8 @@ if args[1] then
command = 'args[1]' command = 'args[1]'
sandboxEnv.args = args sandboxEnv.args = args
page:setResult(args[1]) page:setResult(args[1])
page:setPrompt(command)
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@@ -4,7 +4,6 @@ local Socket = require('opus.socket')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local device = _G.device local device = _G.device
local network = _G.network local network = _G.network
local os = _G.os local os = _G.os
@@ -56,6 +55,31 @@ local page = UI.Page {
columns = gridColumns, columns = gridColumns,
sortColumn = 'label', sortColumn = 'label',
autospace = true, autospace = true,
getRowTextColor = function(self, row, selected)
if not row.active then
return 'lightGray'
end
return UI.Grid.getRowTextColor(self, row, selected)
end,
getDisplayValues = function(_, row)
row = Util.shallowCopy(row)
if row.uptime then
if row.uptime < 60 then
row.uptime = string.format("%ds", math.floor(row.uptime))
elseif row.uptime < 3600 then
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
else
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
end
end
if row.fuel then
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
end
if row.distance then
row.distance = Util.toBytes(Util.round(row.distance, 1))
end
return row
end,
}, },
ports = UI.SlideOut { ports = UI.SlideOut {
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
@@ -72,17 +96,22 @@ local page = UI.Page {
sortColumn = 'port', sortColumn = 'port',
autospace = true, autospace = true,
}, },
eventHandler = function(self, event)
if event.type == 'grid_select' then
shell.openForegroundTab('Sniff ' .. event.selected.port)
end
return UI.SlideOut.eventHandler(self, event)
end,
}, },
help = UI.SlideOut { help = UI.SlideOut {
backgroundColor = colors.cyan,
x = 5, ex = -5, height = 8, y = -8, x = 5, ex = -5, height = 8, y = -8,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'Network Help', title = 'Network Help',
event = 'slide_hide', event = 'slide_hide',
}, },
text = UI.TextArea { text = UI.TextArea {
x = 2, y = 2, x = 1, y = 2,
backgroundColor = colors.cyan, marginLeft = 1,
value = [[ value = [[
In order to connect to another computer: In order to connect to another computer:
@@ -127,13 +156,6 @@ local function sendCommand(host, command)
end end
end end
function page.ports:eventHandler(event)
if event.type == 'grid_select' then
shell.openForegroundTab('Sniff ' .. event.selected.port)
end
return UI.SlideOut.eventHandler(self, event)
end
function page.ports.grid:update() function page.ports.grid:update()
local transport = network:getTransport() local transport = network:getTransport()
@@ -230,7 +252,7 @@ function page:eventHandler(event)
Config.update('network', config) Config.update('network', config)
elseif event.type == 'quit' then elseif event.type == 'quit' then
Event.exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@@ -243,33 +265,6 @@ function page.menuBar:getActive(menuItem)
return menuItem.noCheck or not not t return menuItem.noCheck or not not t
end end
function page.grid:getRowTextColor(row, selected)
if not row.active then
return colors.lightGray
end
return UI.Grid.getRowTextColor(self, row, selected)
end
function page.grid:getDisplayValues(row)
row = Util.shallowCopy(row)
if row.uptime then
if row.uptime < 60 then
row.uptime = string.format("%ds", math.floor(row.uptime))
elseif row.uptime < 3600 then
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
else
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
end
end
if row.fuel then
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
end
if row.distance then
row.distance = Util.toBytes(Util.round(row.distance, 1))
end
return row
end
Event.onInterval(1, function() Event.onInterval(1, function()
page.grid:update() page.grid:update()
page.grid:draw() page.grid:draw()
@@ -295,4 +290,4 @@ if not device.wireless_modem then
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@@ -1,4 +1,5 @@
local Alt = require('opus.alternate') local Alt = require('opus.alternate')
local Array = require('opus.array')
local class = require('opus.class') local class = require('opus.class')
local Config = require('opus.config') local Config = require('opus.config')
local Event = require('opus.event') local Event = require('opus.event')
@@ -9,7 +10,6 @@ local Tween = require('opus.ui.tween')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local device = _G.device local device = _G.device
local fs = _G.fs local fs = _G.fs
local os = _G.os local os = _G.os
@@ -18,6 +18,12 @@ local shell = _ENV.shell
local term = _G.term local term = _G.term
local turtle = _G.turtle local turtle = _G.turtle
--[[
turtle: 39x13
computer: 51x19
pocket: 26x20
]]
if not _ENV.multishell then if not _ENV.multishell then
error('multishell is required') error('multishell is required')
end end
@@ -26,6 +32,15 @@ local REGISTRY_DIR = 'usr/.registry'
local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\ local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\
\0307\0318\153\153\153\153\153\ \0307\0318\153\153\153\153\153\
\0308\0317\153\153\153\153\153") \0308\0317\153\153\153\153\153")
local TRANS_ICON = NFT.parse("\0302\0312\32\32\32\32\32\
\0302\0312\32\32\32\32\32\
\0302\0312\32\32\32\32\32")
-- overview
local uid = _ENV.multishell.getCurrent()
device.keyboard.addHotkey('control-o', function()
_ENV.multishell.setFocus(uid)
end)
UI:configure('Overview', ...) UI:configure('Overview', ...)
@@ -59,6 +74,7 @@ local function parseIcon(iconText)
if icon.height > 3 or icon.width > 8 then if icon.height > 3 or icon.width > 8 then
error('Must be an NFT image - 3 rows, 8 cols max') error('Must be an NFT image - 3 rows, 8 cols max')
end end
NFT.transparency(icon)
end end
return icon return icon
end) end)
@@ -70,45 +86,38 @@ local function parseIcon(iconText)
return s, m return s, m
end end
UI.VerticalTabBar = class(UI.TabBar)
function UI.VerticalTabBar:setParent()
self.x = 1
self.width = 8
self.height = nil
self.ey = -2
UI.TabBar.setParent(self)
for k,c in pairs(self.children) do
c.x = 1
c.y = k + 1
c.ox, c.oy = c.x, c.y
c.ow = 8
c.width = 8
end
end
local cx = 9
local cy = 1
local page = UI.Page { local page = UI.Page {
container = UI.Viewport { container = UI.Viewport {
x = cx, x = 9, y = 1,
y = cy, },
tabBar = UI.TabBar {
ey = -2,
width = 8,
selectedBackgroundColor = 'primary',
backgroundColor = 'tertiary',
layout = function(self)
self.height = nil
UI.TabBar.layout(self)
end,
}, },
tray = UI.Window { tray = UI.Window {
y = -1, width = 8, y = -1, width = 8,
backgroundColor = colors.lightGray, backgroundColor = 'tertiary',
newApp = UI.Button { newApp = UI.FlatButton {
x = 2,
text = '+', event = 'new', text = '+', event = 'new',
}, },
--[[ mode = UI.FlatButton {
volume = UI.Button { x = 4,
x = 3, text = '=', event = 'display_mode',
text = '\15', event = 'volume', },
},]] help = UI.FlatButton {
x = 6,
text = '?', event = 'help',
},
}, },
editor = UI.SlideOut { editor = UI.SlideOut {
y = -12, height = 12, y = -12, height = 12,
backgroundColor = colors.cyan,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'Edit Application', title = 'Edit Application',
event = 'slide_hide', event = 'slide_hide',
@@ -116,7 +125,7 @@ local page = UI.Page {
form = UI.Form { form = UI.Form {
y = 2, ey = -2, y = 2, ey = -2,
[1] = UI.TextEntry { [1] = UI.TextEntry {
formLabel = 'Title', formKey = 'title', limit = 11, help = 'Application title', formLabel = 'Title', formKey = 'title', limit = 11, width = 13, help = 'Application title',
required = true, required = true,
}, },
[2] = UI.TextEntry { [2] = UI.TextEntry {
@@ -124,23 +133,50 @@ local page = UI.Page {
required = true, required = true,
}, },
[3] = UI.TextEntry { [3] = UI.TextEntry {
formLabel = 'Category', formKey = 'category', limit = 11, help = 'Category of application', formLabel = 'Category', formKey = 'category', limit = 6, width = 8, help = 'Category of application',
required = true, required = true,
}, },
iconFile = UI.TextEntry { editIcon = UI.Button {
x = 11, ex = -12, y = 7, x = 11, y = 6,
limit = 128, help = 'Path to icon file', text = 'Edit', event = 'editIcon', help = 'Edit icon file',
shadowText = 'Path to icon file',
}, },
loadIcon = UI.Button { loadIcon = UI.Button {
x = 11, y = 9, x = 11, y = 8,
text = 'Load', event = 'loadIcon', help = 'Load icon file',
},
helpIcon = UI.Button {
x = 11, y = 8,
text = 'Load', event = 'loadIcon', help = 'Load icon file', text = 'Load', event = 'loadIcon', help = 'Load icon file',
}, },
image = UI.NftImage { image = UI.NftImage {
backgroundColor = colors.black, backgroundColor = 'black',
y = 7, x = 2, height = 3, width = 8, y = 6, x = 2, height = 3, width = 8,
}, },
}, },
file_open = UI.FileSelect {
modal = true,
enable = function() end,
transitionHint = 'expandUp',
show = function(self)
UI.FileSelect.enable(self)
self:focusFirst()
self:draw()
end,
disable = function(self)
UI.FileSelect.disable(self)
self.parent:focusFirst()
-- need to recapture as we are opening a modal within another modal
self.parent:capture(self.parent)
end,
eventHandler = function(self, event)
if event.type == 'select_cancel' then
self:disable()
elseif event.type == 'select_file' then
self:disable()
end
return UI.FileSelect.eventHandler(self, event)
end,
},
notification = UI.Notification(), notification = UI.Notification(),
statusBar = UI.StatusBar(), statusBar = UI.StatusBar(),
}, },
@@ -151,6 +187,7 @@ local page = UI.Page {
f = 'files', f = 'files',
s = 'shell', s = 'shell',
l = 'lua', l = 'lua',
n = 'network',
[ 'control-n' ] = 'new', [ 'control-n' ] = 'new',
delete = 'delete', delete = 'delete',
}, },
@@ -198,7 +235,7 @@ local function loadApplications()
return requirements[a.requires] return requirements[a.requires]
end end
return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run) return true
end) end)
local categories = { } local categories = { }
@@ -208,6 +245,7 @@ local function loadApplications()
categories[f.category] = true categories[f.category] = true
table.insert(buttons, { table.insert(buttons, {
text = f.category, text = f.category,
width = 8,
selected = config.currentCategory == f.category selected = config.currentCategory == f.category
}) })
end end
@@ -215,13 +253,13 @@ local function loadApplications()
table.sort(buttons, function(a, b) return a.text < b.text end) table.sort(buttons, function(a, b) return a.text < b.text end)
table.insert(buttons, 1, { text = 'Recent' }) table.insert(buttons, 1, { text = 'Recent' })
Util.removeByValue(page.children, page.tabBar) for k,v in pairs(buttons) do
v.x = 1
v.y = k + 1
end
page:add { page.tabBar.children = { }
tabBar = UI.VerticalTabBar { page.tabBar:addButtons(buttons)
buttons = buttons,
},
}
--page.tabBar:selectTab(config.currentCategory or 'Apps') --page.tabBar:selectTab(config.currentCategory or 'Apps')
page.container:setCategory(config.currentCategory or 'Apps') page.container:setCategory(config.currentCategory or 'Apps')
@@ -236,7 +274,6 @@ UI.Icon.defaults = {
function UI.Icon:eventHandler(event) function UI.Icon:eventHandler(event)
if event.type == 'mouse_click' then if event.type == 'mouse_click' then
self:setFocus(self.button) self:setFocus(self.button)
--self:emit({ type = self.button.event, button = self.button })
return true return true
elseif event.type == 'mouse_doubleclick' then elseif event.type == 'mouse_doubleclick' then
self:emit({ type = self.button.event, button = self.button }) self:emit({ type = self.button.event, button = self.button })
@@ -252,37 +289,23 @@ function page.container:setCategory(categoryName, animate)
self.children = { } self.children = { }
self:reset() self:reset()
local function filter(it, f) local filtered = { }
local ot = { }
for _,v in pairs(it) do
if f(v) then
table.insert(ot, v)
end
end
return ot
end
local filtered
if categoryName == 'Recent' then if categoryName == 'Recent' then
filtered = { }
for _,v in ipairs(config.Recent) do for _,v in ipairs(config.Recent) do
local app = Util.find(applications, 'key', v) local app = Util.find(applications, 'key', v)
if app then -- and fs.exists(app.run) then if app then
table.insert(filtered, app) table.insert(filtered, app)
end end
end end
else else
filtered = filter(applications, function(a) filtered = Array.filter(applications, function(a)
return a.category == categoryName -- and fs.exists(a.run) return a.category == categoryName
end) end)
table.sort(filtered, function(a, b) return a.title < b.title end) table.sort(filtered, function(a, b) return a.title < b.title end)
end end
for _,program in ipairs(filtered) do for _,program in ipairs(filtered) do
local icon local icon
if extSupport and program.iconExt then if extSupport and program.iconExt then
icon = parseIcon(program.iconExt) icon = parseIcon(program.iconExt)
@@ -297,27 +320,43 @@ function page.container:setCategory(categoryName, animate)
local title = ellipsis(program.title, 8) local title = ellipsis(program.title, 8)
local width = math.max(icon.width + 2, #title + 2) local width = math.max(icon.width + 2, #title + 2)
table.insert(self.children, UI.Icon({ if config.listMode then
width = width, table.insert(self.children, UI.Icon {
image = UI.NftImage({ width = self.width - 2,
x = math.floor((width - icon.width) / 2) + 1, height = 1,
image = icon, UI.Button {
width = 5, x = 1, ex = -1,
height = 3, text = program.title,
}), centered = false,
button = UI.Button({ backgroundColor = self:getProperty('backgroundColor'),
x = math.floor((width - #title - 2) / 2) + 1, backgroundFocusColor = 'gray',
y = 4, textColor = 'white',
text = title, textFocusColor = 'white',
backgroundColor = self.backgroundColor, event = 'button',
backgroundFocusColor = colors.gray, app = program,
textColor = colors.white, }
textFocusColor = colors.white, })
width = #title + 2, else
event = 'button', table.insert(self.children, UI.Icon({
app = program, width = width,
}), image = UI.NftImage({
})) x = math.floor((width - icon.width) / 2) + 1,
image = icon,
}),
button = UI.Button({
x = math.floor((width - #title - 2) / 2) + 1,
y = 4,
text = title,
backgroundColor = self:getProperty('backgroundColor'),
backgroundFocusColor = 'gray',
textColor = 'white',
textFocusColor = 'white',
width = #title + 2,
event = 'button',
app = program,
}),
}))
end
end end
local gutter = 2 local gutter = 2
@@ -327,7 +366,8 @@ function page.container:setCategory(categoryName, animate)
local col, row = gutter, 2 local col, row = gutter, 2
local count = #self.children local count = #self.children
local r = math.random(1, 5) local r = math.random(1, 7)
local frames = 5
-- reposition all children -- reposition all children
for k,child in ipairs(self.children) do for k,child in ipairs(self.children) do
if r == 1 then if r == 1 then
@@ -349,19 +389,27 @@ function page.container:setCategory(categoryName, animate)
child.x = self.width child.x = self.width
child.y = self.height - 3 child.y = self.height - 3
end end
elseif r == 6 then
child.x = col
child.y = 1
elseif r == 7 then
child.x = 1
child.y = self.height - 3
end end
child.tween = Tween.new(6, child, { x = col, y = row }, 'linear') child.tween = Tween.new(frames, child, { x = col, y = row }, 'inQuad')
if not animate then if not animate then
child.x = col child.x = col
child.y = row child.y = row
end end
self:setViewHeight(row + (config.listMode and 1 or 4))
if k < count then if k < count then
col = col + child.width col = col + child.width
if col + self.children[k + 1].width + gutter - 2 > self.width then if col + self.children[k + 1].width + gutter - 2 > self.width then
col = gutter col = gutter
row = row + 5 row = row + (config.listMode and 1 or 5)
end end
end end
end end
@@ -371,15 +419,12 @@ function page.container:setCategory(categoryName, animate)
local function transition() local function transition()
local i = 1 local i = 1
return function() return function()
self:clear()
for _,child in pairs(self.children) do for _,child in pairs(self.children) do
child.tween:update(1) child.tween:update(1)
child.x = math.floor(child.x) child:move(math.floor(child.x), math.floor(child.y))
child.y = math.floor(child.y)
child:draw()
end end
i = i + 1 i = i + 1
return i < 7 return i <= frames
end end
end end
self:addTransition(transition) self:addTransition(transition)
@@ -429,6 +474,12 @@ function page:eventHandler(event)
elseif event.type == 'files' then elseif event.type == 'files' then
shell.switchTab(shell.openTab(Alt.get('files'))) shell.switchTab(shell.openTab(Alt.get('files')))
elseif event.type == 'network' then
shell.switchTab(shell.openTab('network'))
elseif event.type == 'help' then
shell.switchTab(shell.openTab('Help Overview'))
elseif event.type == 'focus_change' then elseif event.type == 'focus_change' then
if event.focused.parent.UIElement == 'Icon' then if event.focused.parent.UIElement == 'Icon' then
event.focused.parent:scrollIntoView() event.focused.parent:scrollIntoView()
@@ -463,6 +514,13 @@ function page:eventHandler(event)
end end
self.editor:show({ category = category }) self.editor:show({ category = category })
elseif event.type == 'display_mode' then
config.listMode = not config.listMode
Config.update('Overview', config)
loadApplications()
self:refresh()
self:draw()
elseif event.type == 'edit' then elseif event.type == 'edit' then
local focused = page:getFocused() local focused = page:getFocused()
if focused.app then if focused.app then
@@ -470,7 +528,7 @@ function page:eventHandler(event)
end end
else else
UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
end end
return true return true
end end
@@ -492,11 +550,6 @@ function page.editor:show(app)
self:focusFirst() self:focusFirst()
end end
function page.editor.form.image:draw()
self:clear()
UI.NftImage.draw(self)
end
function page.editor:updateApplications(app) function page.editor:updateApplications(app)
if not app.key then if not app.key then
app.key = SHA.compute(app.title) app.key = SHA.compute(app.title)
@@ -506,36 +559,51 @@ function page.editor:updateApplications(app)
loadApplications() loadApplications()
end end
function page.editor:loadImage(filename)
local s, m = pcall(function()
local iconLines = Util.readFile(filename)
if not iconLines then
error('Must be an NFT image - 3 rows, 8 cols max')
end
local icon, m = parseIcon(iconLines)
if not icon then
error(m)
end
if extSupport then
self.form.values.iconExt = iconLines
else
self.form.values.icon = iconLines
end
self.form.image:setImage(icon)
self.form.image:draw()
end)
if not s and m then
local msg = m:gsub('.*: (.*)', '%1')
self.notification:error(msg)
end
end
function page.editor:eventHandler(event) function page.editor:eventHandler(event)
if event.type == 'form_cancel' or event.type == 'cancel' then if event.type == 'form_cancel' or event.type == 'cancel' then
self:hide() self:hide()
elseif event.type == 'focus_change' then elseif event.type == 'focus_change' then
self.statusBar:setStatus(event.focused.help or '') self.statusBar:setStatus(event.focused.help or '')
self.statusBar:draw()
elseif event.type == 'editIcon' then
local filename = '/tmp/editing.nft'
NFT.save(self.form.image.image or TRANS_ICON, filename)
local success = shell.run('pain.lua ' .. filename)
self.parent:dirty(true)
if success then
self:loadImage(filename)
end
elseif event.type == 'select_file' then
self:loadImage(event.file)
elseif event.type == 'loadIcon' then elseif event.type == 'loadIcon' then
local s, m = pcall(function() self.file_open:show()
local iconLines = Util.readFile(self.form.iconFile.value)
if not iconLines then
error('Must be an NFT image - 3 rows, 8 cols max')
end
local icon, m = parseIcon(iconLines)
if not icon then
error(m)
end
if extSupport then
self.form.values.iconExt = iconLines
else
self.form.values.icon = iconLines
end
self.form.image:setImage(icon)
self.form.image:draw()
end)
if not s and m then
local msg = m:gsub('.*: (.*)', '%1')
self.notification:error(msg)
end
elseif event.type == 'form_invalid' then elseif event.type == 'form_invalid' then
self.notification:error(event.message) self.notification:error(event.message)
@@ -544,8 +612,6 @@ function page.editor:eventHandler(event)
local values = self.form.values local values = self.form.values
self:hide() self:hide()
self:updateApplications(values) self:updateApplications(values)
--page:refresh()
--page:draw()
config.currentCategory = values.category config.currentCategory = values.category
Config.update('Overview', config) Config.update('Overview', config)
os.queueEvent('overview_refresh') os.queueEvent('overview_refresh')
@@ -555,10 +621,6 @@ function page.editor:eventHandler(event)
return true return true
end end
UI:setPages({
main = page,
})
local function reload() local function reload()
loadApplications() loadApplications()
page:refresh() page:refresh()
@@ -584,5 +646,4 @@ end)
loadApplications() loadApplications()
UI:setPage(page) UI:setPage(page)
UI:start()
UI:pullEvents()

View File

@@ -44,7 +44,6 @@ local page = UI.Page {
marginRight = 0, marginLeft = 0, marginRight = 0, marginLeft = 0,
}, },
action = UI.SlideOut { action = UI.SlideOut {
backgroundColor = colors.cyan,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
event = 'hide-action', event = 'hide-action',
}, },
@@ -184,7 +183,7 @@ function page:eventHandler(event)
self.action.button:draw() self.action.button:draw()
elseif event.type == 'quit' then elseif event.type == 'quit' then
UI:exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@@ -196,4 +195,4 @@ Packages:downloadList()
page:loadPackages() page:loadPackages()
page:sync() page:sync()
UI:pullEvents() UI:start()

View File

@@ -27,4 +27,4 @@ kernel.hook('kernel_focus', function(_, eventData)
end end
end) end)
os.pullEventRaw('kernel_halt') os.pullEventRaw('kernel_halt')

View File

@@ -5,14 +5,13 @@ local Util = require('opus.util')
local colors = _G.colors local colors = _G.colors
local device = _G.device local device = _G.device
local textutils = _G.textutils local textutils = _G.textutils
local peripheral = _G.peripheral
local multishell = _ENV.multishell local multishell = _ENV.multishell
local gridColumns = {} local gridColumns = {}
table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' }) table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' }) table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' }) table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' })
if UI.defaultDevice.width > 50 then if UI.term.width > 50 then
table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' }) table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' })
end end
table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' }) table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' })
@@ -42,12 +41,13 @@ local page = UI.Page {
configSlide = UI.SlideOut { configSlide = UI.SlideOut {
y = -11, y = -11,
titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close' }, titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close', backgroundColor = colors.black },
accelerators = { ['backspace'] = 'config_close' }, accelerators = { ['backspace'] = 'config_close' },
configTabs = UI.Tabs { configTabs = UI.Tabs {
y = 2, y = 2,
filterTab = UI.Tab { filterTab = UI.Tab {
tabTitle = 'Filter', tabTitle = 'Filter',
noFill = true,
filterGridText = UI.Text { filterGridText = UI.Text {
x = 2, y = 2, x = 2, y = 2,
value = 'ID filter', value = 'ID filter',
@@ -130,7 +130,6 @@ local page = UI.Page {
title = 'Packet Information', title = 'Packet Information',
event = 'packet_close', event = 'packet_close',
}, },
backgroundColor = colors.cyan,
accelerators = { accelerators = {
['backspace'] = 'packet_close', ['backspace'] = 'packet_close',
['left'] = 'prev_packet', ['left'] = 'prev_packet',
@@ -280,7 +279,7 @@ function page.packetSlide:eventHandler(event)
end end
function page.packetGrid:getDisplayValues(row) function page.packetGrid:getDisplayValues(row)
local row = Util.shallowCopy(row) row = Util.shallowCopy(row)
row.distance = Util.toBytes(Util.round(row.distance), 2) row.distance = Util.toBytes(Util.round(row.distance), 2)
return row return row
end end
@@ -307,14 +306,16 @@ end
function page:enable() function page:enable()
modemConfig.modems = {} modemConfig.modems = {}
peripheral.find('modem', function(side, dev) Util.each(_G.device, function(dev)
modemConfig.modems[side] = { if dev.type == "modem" then
type = dev.isWireless() and 'Wireless' or 'Wired', modemConfig.modems[dev.side] = {
side = side, type = dev.isWireless() and 'Wireless' or 'Wired',
openChannels = { }, side = dev.side,
device = dev, openChannels = { },
loaded = false device = dev,
} loaded = false
}
end
end) end)
modemConfig.currentModem = device.wireless_modem and modemConfig.currentModem = device.wireless_modem and
modemConfig.modems[device.wireless_modem.side] or modemConfig.modems[device.wireless_modem.side] or
@@ -354,7 +355,7 @@ function page:eventHandler(event)
self.packetSlide:show(event.selected) self.packetSlide:show(event.selected)
elseif event.type == 'quit' then elseif event.type == 'quit' then
Event.exitPullEvents() UI:quit()
else return UI.Page.eventHandler(self, event) else return UI.Page.eventHandler(self, event)
end end
@@ -384,4 +385,4 @@ if args[1] then
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@@ -11,7 +11,7 @@ local systemPage = UI.Page {
settings = UI.Tab { settings = UI.Tab {
tabTitle = 'Category', tabTitle = 'Category',
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 2, x = 2, y = 2, ex = -2, ey = -2,
columns = { columns = {
{ heading = 'Name', key = 'name' }, { heading = 'Name', key = 'name' },
{ heading = 'Description', key = 'description' }, { heading = 'Description', key = 'description' },
@@ -35,14 +35,14 @@ function systemPage.tabs.settings:eventHandler(event)
tab:disable() tab:disable()
end end
systemPage.tabs:selectTab(tab) systemPage.tabs:selectTab(tab)
self.parent:draw() --self.parent:draw()
return true return true
end end
end end
function systemPage:eventHandler(event) function systemPage:eventHandler(event)
if event.type == 'quit' then if event.type == 'quit' then
UI:exitPullEvents() UI:quit()
elseif event.type == 'success_message' then elseif event.type == 'success_message' then
self.notification:success(event.message) self.notification:success(event.message)
@@ -82,4 +82,4 @@ local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
systemPage.tabs.settings.grid:setValues(plugins) systemPage.tabs.settings.grid:setValues(plugins)
UI:setPage(systemPage) UI:setPage(systemPage)
UI:pullEvents() UI:start()

View File

@@ -3,6 +3,7 @@ local UI = require('opus.ui')
local kernel = _G.kernel local kernel = _G.kernel
local multishell = _ENV.multishell local multishell = _ENV.multishell
local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines
UI:configure('Tasks', ...) UI:configure('Tasks', ...)
@@ -21,43 +22,41 @@ local page = UI.Page {
{ heading = 'Status', key = 'status' }, { heading = 'Status', key = 'status' },
{ heading = 'Time', key = 'timestamp' }, { heading = 'Time', key = 'timestamp' },
}, },
values = kernel.routines, values = tasks,
sortColumn = 'uid', sortColumn = 'uid',
autospace = true, autospace = true,
getDisplayValues = function (_, row)
local elapsed = os.clock()-row.timestamp
return {
uid = row.uid,
title = row.title,
status = row.isDead and 'error' or coroutine.status(row.co),
timestamp = elapsed < 60 and
string.format("%ds", math.floor(elapsed)) or
string.format("%sm", math.floor(elapsed/6)/10),
}
end
}, },
accelerators = { accelerators = {
[ 'control-q' ] = 'quit', [ 'control-q' ] = 'quit',
space = 'activate', space = 'activate',
t = 'terminate', t = 'terminate',
}, },
} eventHandler = function (self, event)
local t = self.grid:getSelected()
function page:eventHandler(event) if t then
local t = self.grid:getSelected() if event.type == 'activate' or event.type == 'grid_select' then
if t then multishell.setFocus(t.uid)
if event.type == 'activate' or event.type == 'grid_select' then elseif event.type == 'terminate' then
multishell.setFocus(t.uid) multishell.terminate(t.uid)
elseif event.type == 'terminate' then end
multishell.terminate(t.uid)
end end
if event.type == 'quit' then
UI:quit()
end
UI.Page.eventHandler(self, event)
end end
if event.type == 'quit' then }
Event.exitPullEvents()
end
UI.Page.eventHandler(self, event)
end
function page.grid:getDisplayValues(row)
local elapsed = os.clock()-row.timestamp
return {
uid = row.uid,
title = row.title,
status = row.isDead and 'error' or coroutine.status(row.co),
timestamp = elapsed < 60 and
string.format("%ds", math.floor(elapsed)) or
string.format("%sm", math.floor(elapsed/6)/10),
}
end
Event.onInterval(1, function() Event.onInterval(1, function()
page.grid:update() page.grid:update()
@@ -66,4 +65,4 @@ Event.onInterval(1, function()
end) end)
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@@ -60,6 +60,12 @@ local page = UI.Page {
x = 3, ex = -3, y = 4, ey = -3, x = 3, ex = -3, y = 4, ey = -3,
value = string.format(labelIntro, Ansi.white), value = string.format(labelIntro, Ansi.white),
}, },
validate = function (self)
if self.label.value then
os.setComputerLabel(self.label.value)
end
return true
end,
}, },
password = UI.WizardPage { password = UI.WizardPage {
index = 3, index = 3,
@@ -73,23 +79,18 @@ local page = UI.Page {
mask = true, mask = true,
shadowText = 'password', shadowText = 'password',
}, },
--[[
groupLabel = UI.Text {
x = 3, y = 3,
value = 'Group'
},
group = UI.TextEntry {
x = 12, ex = -3, y = 3,
limit = 32,
shadowText = 'network group',
},
]]
intro = UI.TextArea { intro = UI.TextArea {
textColor = colors.yellow, textColor = colors.yellow,
inactive = true, inactive = true,
x = 3, ex = -3, y = 5, ey = -3, x = 3, ex = -3, y = 5, ey = -3,
value = string.format(passwordIntro, Ansi.white), value = string.format(passwordIntro, Ansi.white),
}, },
validate = function (self)
if type(self.newPass.value) == "string" and #self.newPass.value > 0 then
Security.updatePassword(SHA.compute(self.newPass.value))
end
return true
end,
}, },
packages = UI.WizardPage { packages = UI.WizardPage {
index = 4, index = 4,
@@ -119,25 +120,6 @@ local page = UI.Page {
notification = UI.Notification { }, notification = UI.Notification { },
} }
function page.wizard.pages.label:validate()
os.setComputerLabel(self.label.value)
return true
end
function page.wizard.pages.password:validate()
if type(self.newPass.value) == "string" and #self.newPass.value > 0 then
Security.updatePassword(SHA.compute(self.newPass.value))
end
--[[
if #self.group.value > 0 then
local config = Config.load('os')
config.group = self.group.value
Config.update('os', config)
end
]]
return true
end
function page:eventHandler(event) function page:eventHandler(event)
if event.type == 'skip' then if event.type == 'skip' then
self.wizard:emit({ type = 'nextView' }) self.wizard:emit({ type = 'nextView' })
@@ -149,7 +131,7 @@ function page:eventHandler(event)
shell.openForegroundTab('PackageManager') shell.openForegroundTab('PackageManager')
elseif event.type == 'wizard_complete' or event.type == 'cancel' then elseif event.type == 'wizard_complete' or event.type == 'cancel' then
UI.exitPullEvents() UI:quit()
else else
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
@@ -158,4 +140,4 @@ function page:eventHandler(event)
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

39
sys/apps/fileui.lua Normal file
View File

@@ -0,0 +1,39 @@
local UI = require('opus.ui')
local Util = require('opus.util')
local shell = _ENV.shell
local multishell = _ENV.multishell
-- fileui [--path=path] [--exec=filename] [--title=title]
local page = UI.Page {
fileselect = UI.FileSelect { },
eventHandler = function(self, event)
if event.type == 'select_file' then
self.selected = event.file
UI:quit()
elseif event.type == 'select_cancel' then
UI:quit()
end
return UI.FileSelect.eventHandler(self, event)
end,
}
local _, args = Util.parse(...)
if args.title and multishell then
multishell.setTitle(multishell.getCurrent(), args.title)
end
UI:setPage(page, args.path)
UI:start()
UI.term:setCursorBlink(false)
if args.exec and page.selected then
shell.openForegroundTab(string.format('%s %s', args.exec, page.selected))
return
end
return page.selected

195
sys/apps/inspect.lua Normal file
View File

@@ -0,0 +1,195 @@
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local multishell = _ENV.multishell
local name = ({ ... })[1] or error('Syntax: inspect COMPONENT')
local events = { }
local page, lastEvent, focused
local function isRelevant(el)
return page.testContainer == el or el.parent and isRelevant(el.parent)
end
local emitter = UI.Window.emit
function UI.Window:emit(event)
if event ~= lastEvent and isRelevant(self) then
lastEvent = event
local t = { }
for k,v in pairs(event) do
if k ~= 'type' and k ~= 'recorded' then
table.insert(t, k .. ':' .. (type(v) == 'table' and (v.UIElement and v.uid or 'tbl') or tostring(v)))
end
end
table.insert(events, 1, { type = event.type, value = table.concat(t, ' '), raw = event })
while #events > 20 do
table.remove(events)
end
page.tabs.events.grid:update()
if page.tabs.events.enabled then
page.tabs.events.grid:draw()
end
end
return emitter(self, event)
end
-- do not load component until emit hook is in place
local component = UI[name] and UI[name]() or error('Invalid component')
if not component.example then
error('No example present')
end
page = UI.Page {
testContainer = UI.Window {
ey = '50%',
testing = component.example(),
},
tabs = UI.Tabs {
backgroundColor = colors.red,
y = '50%',
properties = UI.Tab {
tabTitle = 'Properties',
grid = UI.ScrollingGrid {
headerBackgroundColor = colors.red,
sortColumn = 'key',
columns = {
{ heading = 'key', key = 'key' },
{ heading = 'value', key = 'value', }
},
accelerators = {
grid_select = 'edit_property',
},
},
},
methodsTab = UI.Tab {
index = 2,
tabTitle = 'Methods',
grid = UI.ScrollingGrid {
ex = '50%',
headerBackgroundColor = colors.red,
sortColumn = 'key',
columns = {
{ heading = 'key', key = 'key' },
},
},
docs = UI.TextArea {
x = '50%',
backgroundColor = colors.black,
},
eventHandler = function (self, event)
if event.type == 'grid_focus_row' and focused then
self.docs:setText(focused:getDoc(event.selected.key) or '')
end
end,
},
events = UI.Tab {
index = 1,
tabTitle = 'Events',
UI.MenuBar {
y = -1,
backgroundColor = colors.red,
buttons = {
{ text = 'Clear' },
}
},
grid = UI.ScrollingGrid {
ey = -2,
headerBackgroundColor = colors.red,
values = events,
autospace = true,
columns = {
{ heading = 'type', key = 'type' },
{ heading = 'value', key = 'value', }
},
},
eventHandler = function (self, event)
if event.type == 'button_press' then
Util.clear(self.grid.values)
self.grid:update()
self.grid:draw()
elseif event.type == 'grid_select' then
multishell.openTab({
path = 'sys/apps/Lua.lua',
args = { event.selected.raw },
focused = true,
})
end
end
}
},
editor = UI.SlideOut {
y = -4, height = 4,
backgroundColor = colors.green,
titleBar = UI.TitleBar {
event = 'editor_cancel',
title = 'Enter value',
},
entry = UI.TextEntry {
y = 3, x = 2, ex = 10,
accelerators = {
enter = 'editor_apply',
},
},
},
accelerators = {
['shift-right'] = 'size',
['shift-left' ] = 'size',
['shift-up' ] = 'size',
['shift-down' ] = 'size',
},
eventHandler = function (self, event)
if event.type == 'focus_change' and isRelevant(event.focused) then
focused = event.focused
local t = { }
for k,v in pairs(event.focused) do
table.insert(t, {
key = k,
value = tostring(v),
})
end
self.tabs.properties.grid:setValues(t)
self.tabs.properties.grid:draw()
t = { }
for k,v in pairs(getmetatable(event.focused)) do
if type(v) == 'function' then
table.insert(t, {
key = k,
})
end
end
self.tabs.methodsTab.grid:setValues(t)
self.tabs.methodsTab.grid:draw()
elseif event.type == 'edit_property' then
self.editor.entry.value = event.selected.value
self.editor:show()
elseif event.type == 'editor_cancel' then
self.editor:hide()
elseif event.type == 'editor_apply' then
self.editor:hide()
elseif event.type == 'size' then
local sizing = {
['shift-right'] = { 1, 0 },
['shift-left' ] = { -1, 0 },
['shift-up' ] = { 0, -1 },
['shift-down' ] = { 0, 1 },
}
self.ox = math.max(self.ox + sizing[event.ie.code][1], 1)
self.oy = math.max(self.oy + sizing[event.ie.code][2], 1)
UI.term:clear()
self:resize()
self:draw()
end
return UI.Page.eventHandler(self, event)
end
}
UI:setPage(page)
UI:start()

View File

@@ -10,30 +10,30 @@ local keyPairs = { }
local function generateKeyPair() local function generateKeyPair()
local key = { } local key = { }
for _ = 1, 32 do for _ = 1, 32 do
table.insert(key, ("%02x"):format(math.random(0, 0xFF))) table.insert(key, math.random(0, 0xFF))
end end
local privateKey = Util.hexToByteArray(table.concat(key)) local privateKey = setmetatable(key, Util.byteArrayMT)
return privateKey, ECC.publicKey(privateKey) return privateKey, ECC.publicKey(privateKey)
end end
getmetatable(network).__index.getKeyPair = function() getmetatable(network).__index.getKeyPair = function()
local keys = table.remove(keyPairs) local keys = table.remove(keyPairs)
os.queueEvent('generate_keypair') os.queueEvent('generate_keypair')
if not keys then if not keys then
return generateKeyPair() return generateKeyPair()
end end
return table.unpack(keys) return table.unpack(keys)
end end
-- Generate key pairs in the background as this is a time-consuming process -- Generate key pairs in the background as this is a time-consuming process
Event.on('generate_keypair', function() Event.on('generate_keypair', function()
while true do while true do
os.sleep(5) os.sleep(5)
local timer = Util.timer() local timer = Util.timer()
table.insert(keyPairs, { generateKeyPair() }) table.insert(keyPairs, { generateKeyPair() })
_G._syslog('Generated keypair in ' .. timer()) _G._syslog('Generated keypair in ' .. timer())
if #keyPairs >= 3 then if #keyPairs >= 3 then
break break
end end
end end
end) end)

View File

@@ -66,11 +66,11 @@ local function run(env, ...)
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)')) _ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)'))
end end
if isUrl then tProgramStack[#tProgramStack + 1] = {
tProgramStack[#tProgramStack + 1] = path -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$") path = path, -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$")
else env = env,
tProgramStack[#tProgramStack + 1] = path args = args,
end }
env[ "arg" ] = { [0] = path, table.unpack(args) } env[ "arg" ] = { [0] = path, table.unpack(args) }
local r = { fn(table.unpack(args)) } local r = { fn(table.unpack(args)) }
@@ -278,6 +278,10 @@ function shell.getCompletionInfo()
end end
function shell.getRunningProgram() function shell.getRunningProgram()
return tProgramStack[#tProgramStack] and tProgramStack[#tProgramStack].path
end
function shell.getRunningInfo()
return tProgramStack[#tProgramStack] return tProgramStack[#tProgramStack]
end end
@@ -586,6 +590,7 @@ local function shellRead(history)
end end
local _,cy = term.getCursorPos() local _,cy = term.getCursorPos()
term.setCursorPos(3, cy) term.setCursorPos(3, cy)
entry.value = entry.value or ''
local filler = #entry.value < lastLen local filler = #entry.value < lastLen
and string.rep(' ', lastLen - #entry.value) and string.rep(' ', lastLen - #entry.value)
or '' or ''
@@ -635,6 +640,7 @@ local function shellRead(history)
redraw() redraw()
elseif ie.code == 'tab' then elseif ie.code == 'tab' then
entry.value = entry.value or ''
if entry.pos == #entry.value then if entry.pos == #entry.value then
local cline = autocomplete(entry.value) local cline = autocomplete(entry.value)
if cline then if cline then
@@ -650,6 +656,7 @@ local function shellRead(history)
else else
entry:process(ie) entry:process(ie)
entry.value = entry.value or ''
if entry.textChanged then if entry.textChanged then
redraw() redraw()
elseif entry.posChanged then elseif entry.posChanged then
@@ -666,7 +673,7 @@ local function shellRead(history)
print() print()
term.setCursorBlink( false ) term.setCursorBlink( false )
return entry.value return entry.value or ''
end end
local history = History.load('usr/.shell_history', 25) local history = History.load('usr/.shell_history', 25)

View File

@@ -20,7 +20,7 @@ local aliasTab = UI.Tab {
}, },
}, },
grid = UI.Grid { grid = UI.Grid {
y = 5, x = 2, y = 5, ex = -2, ey = -2,
sortColumn = 'alias', sortColumn = 'alias',
columns = { columns = {
{ heading = 'Alias', key = 'alias' }, { heading = 'Alias', key = 'alias' },

View File

@@ -2,8 +2,6 @@ local Array = require('opus.array')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Preferred', tabTitle = 'Preferred',
description = 'Select preferred applications', description = 'Select preferred applications',
@@ -22,20 +20,19 @@ local tab = UI.Tab {
disableHeader = true, disableHeader = true,
columns = { columns = {
{ key = 'file' }, { key = 'file' },
} },
getRowTextColor = function(self, row)
if row == self.values[1] then
return 'yellow'
end
return UI.Grid.getRowTextColor(self, row)
end,
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar {
values = 'Double-click to set as preferred' values = 'Double-click to set as preferred'
}, },
} }
function tab.choices:getRowTextColor(row)
if row == self.values[1] then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
function tab:updateChoices() function tab:updateChoices()
local app = self.apps:getSelected().name local app = self.apps:getSelected().name
local choices = { } local choices = { }

View File

@@ -2,18 +2,17 @@ local Ansi = require('opus.ansi')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
-- -t80x30
if _G.http.websocket then if _G.http.websocket then
local config = Config.load('cloud') local config = Config.load('cloud')
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Cloud', tabTitle = 'Cloud',
description = 'Cloud Catcher options', description = 'Cloud Catcher options',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
key = UI.TextEntry { key = UI.TextEntry {
x = 3, ex = -3, y = 2, x = 3, ex = -3, y = 3,
limit = 32, limit = 32,
value = config.key, value = config.key,
shadowText = 'Cloud key', shadowText = 'Cloud key',
@@ -22,14 +21,15 @@ if _G.http.websocket then
}, },
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 4, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update_key', event = 'update_key',
}, },
labelText = UI.TextArea { labelText = UI.TextArea {
x = 3, ex = -3, y = 6, x = 2, ex = -2, y = 5, ey = -4,
textColor = colors.yellow, textColor = 'yellow',
marginLeft = 0, marginRight = 0, backgroundColor = 'black',
marginLeft = 1, marginRight = 1, marginTop = 1,
value = string.format( value = string.format(
[[Use a non-changing cloud key. Note that only a single computer can use this session at one time. [[Use a non-changing cloud key. Note that only a single computer can use this session at one time.
To obtain a key, visit: To obtain a key, visit:
@@ -42,7 +42,7 @@ To obtain a key, visit:
function tab:eventHandler(event) function tab:eventHandler(event)
if event.type == 'update_key' then if event.type == 'update_key' then
if #self.key.value > 0 then if self.key.value then
config.key = self.key.value config.key = self.key.value
else else
config.key = nil config.key = nil

View File

@@ -20,8 +20,8 @@ local tab = UI.Tab {
description = 'Visualise HDD and disks usage', description = 'Visualise HDD and disks usage',
drives = UI.ScrollingGrid { drives = UI.ScrollingGrid {
x = 2, y = 1, x = 2, y = 2,
ex = '47%', ey = -7, ex = '47%', ey = -8,
columns = { columns = {
{ heading = 'Drive', key = 'name' }, { heading = 'Drive', key = 'name' },
{ heading = 'Side' ,key = 'side', textColor = colors.yellow } { heading = 'Side' ,key = 'side', textColor = colors.yellow }
@@ -30,7 +30,7 @@ local tab = UI.Tab {
}, },
infos = UI.Grid { infos = UI.Grid {
x = '52%', y = 2, x = '52%', y = 2,
ex = -2, ey = -4, ex = -2, ey = -8,
disableHeader = true, disableHeader = true,
unfocusedBackgroundSelectedColor = colors.black, unfocusedBackgroundSelectedColor = colors.black,
inactive = true, inactive = true,
@@ -40,18 +40,23 @@ local tab = UI.Tab {
{ key = 'value', align = 'right', textColor = colors.yellow }, { key = 'value', align = 'right', textColor = colors.yellow },
} }
}, },
[1] = UI.Window {
x = 2, y = -6, ex = -2, ey = -2,
backgroundColor = colors.black,
},
progress = UI.ProgressBar { progress = UI.ProgressBar {
x = 11, y = -2, x = 11, y = -3,
ex = -2, ex = -3,
}, },
percentage = UI.Text { percentage = UI.Text {
x = 11, y = -3, y = -4, width = 5,
ex = '47%', x = 12,
align = 'center', --align = 'center',
backgroundColor = colors.black,
}, },
icon = UI.NftImage { icon = UI.NftImage {
x = 2, y = -5, x = 2, y = -6, ey = -2,
backgroundColor = colors.black,
image = NFT.parse(NftImages.blank) image = NFT.parse(NftImages.blank)
}, },
} }

View File

@@ -4,11 +4,11 @@ local colors = _G.colors
local peripheral = _G.peripheral local peripheral = _G.peripheral
local settings = _G.settings local settings = _G.settings
local tab = UI.Tab { return peripheral.find('monitor') and UI.Tab {
tabTitle = 'Kiosk', tabTitle = 'Kiosk',
description = 'Kiosk options', description = 'Kiosk options',
form = UI.Form { form = UI.Form {
x = 2, ex = -2, x = 2, y = 2, ex = -2, ey = 5,
manualControls = true, manualControls = true,
monitor = UI.Chooser { monitor = UI.Chooser {
formLabel = 'Monitor', formKey = 'monitor', formLabel = 'Monitor', formKey = 'monitor',
@@ -22,41 +22,36 @@ local tab = UI.Tab {
}, },
help = 'Adjust text scaling', help = 'Adjust text scaling',
}, },
labelText = UI.TextArea {
x = 2, ex = -2, y = 5,
textColor = colors.yellow,
value = 'Settings apply to kiosk mode selected during startup'
},
}, },
} labelText = UI.TextArea {
x = 2, ex = -2, y = 7, ey = -2,
textColor = colors.yellow,
backgroundColor = colors.black,
value = 'Settings apply to kiosk mode selected during startup'
},
enable = function(self)
local choices = { }
function tab:enable() peripheral.find('monitor', function(side)
local choices = { } table.insert(choices, { name = side, value = side })
end)
peripheral.find('monitor', function(side) self.form.monitor.choices = choices
table.insert(choices, { name = side, value = side }) self.form.monitor.value = settings.get('kiosk.monitor')
end)
self.form.monitor.choices = choices self.form.textScale.value = settings.get('kiosk.textscale')
self.form.monitor.value = settings.get('kiosk.monitor')
self.form.textScale.value = settings.get('kiosk.textscale') UI.Tab.enable(self)
end,
UI.Tab.enable(self) eventHandler = function(self, event)
end if event.type == 'choice_change' then
if self.form.monitor.value then
function tab:eventHandler(event) settings.set('kiosk.monitor', self.form.monitor.value)
if event.type == 'choice_change' then end
if self.form.monitor.value then if self.form.textScale.value then
settings.set('kiosk.monitor', self.form.monitor.value) settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end end
if self.form.textScale.value then
settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end end
end }
if peripheral.find('monitor') then
return tab
end

View File

@@ -4,23 +4,26 @@ local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local os = _G.os local os = _G.os
local labelTab = UI.Tab { return UI.Tab {
tabTitle = 'Label', tabTitle = 'Label',
description = 'Set the computer label', description = 'Set the computer label',
labelText = UI.Text { labelText = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Label' value = 'Label'
}, },
label = UI.TextEntry { label = UI.TextEntry {
x = 9, y = 2, ex = -4, x = 9, y = 3, ex = -4,
limit = 32, limit = 32,
value = os.getComputerLabel(), value = os.getComputerLabel(),
accelerators = { accelerators = {
enter = 'update_label', enter = 'update_label',
}, },
}, },
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 3, x = 2, y = 5, ex = -2, ey = -2,
values = { values = {
{ name = '', value = '' }, { name = '', value = '' },
{ name = 'CC version', value = Util.getVersion() }, { name = 'CC version', value = Util.getVersion() },
@@ -30,20 +33,18 @@ local labelTab = UI.Tab {
{ name = 'Computer ID', value = tostring(os.getComputerID()) }, { name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) }, { name = 'Day', value = tostring(os.day()) },
}, },
disableHeader = true,
inactive = true, inactive = true,
columns = { columns = {
{ key = 'name', width = 12 }, { key = 'name', width = 12 },
{ key = 'value' }, { key = 'value', textColor = colors.yellow },
}, },
}, },
eventHandler = function(self, event)
if event.type == 'update_label' and self.label.value then
os.setComputerLabel(self.label.value)
self:emit({ type = 'success_message', message = 'Label updated' })
return true
end
end,
} }
function labelTab:eventHandler(event)
if event.type == 'update_label' then
os.setComputerLabel(self.label.value)
self:emit({ type = 'success_message', message = 'Label updated' })
return true
end
end
return labelTab

View File

@@ -9,12 +9,15 @@ local config = Config.load('multishell')
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Launcher', tabTitle = 'Launcher',
description = 'Set the application launcher', description = 'Set the application launcher',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 5,
},
launcherLabel = UI.Text { launcherLabel = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Launcher', value = 'Launcher',
}, },
launcher = UI.Chooser { launcher = UI.Chooser {
x = 13, y = 2, width = 12, x = 13, y = 3, width = 12,
choices = { choices = {
{ name = 'Overview', value = 'sys/apps/Overview.lua' }, { name = 'Overview', value = 'sys/apps/Overview.lua' },
{ name = 'Shell', value = 'sys/apps/ShellLauncher.lua' }, { name = 'Shell', value = 'sys/apps/ShellLauncher.lua' },
@@ -22,18 +25,20 @@ local tab = UI.Tab {
}, },
}, },
custom = UI.TextEntry { custom = UI.TextEntry {
x = 13, ex = -3, y = 3, x = 13, ex = -3, y = 4,
limit = 128, limit = 128,
shadowText = 'File name', shadowText = 'File name',
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 5, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update', event = 'update',
}, },
labelText = UI.TextArea { labelText = UI.TextArea {
x = 3, ex = -3, y = 7, x = 2, ex = -2, y = 6, ey = -4,
backgroundColor = colors.black,
textColor = colors.yellow, textColor = colors.yellow,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Choose an application launcher', value = 'Choose an application launcher',
}, },
} }

View File

@@ -2,59 +2,61 @@ local Ansi = require('opus.ansi')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local device = _G.device local device = _G.device
local tab = UI.Tab { return UI.Tab {
tabTitle = 'Network', tabTitle = 'Network',
description = 'Networking options', description = 'Networking options',
info = UI.TextArea { info = UI.TextArea {
x = 3, y = 4, x = 2, y = 5, ex = -2, ey = -2,
backgroundColor = colors.black,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = string.format( value = string.format(
[[%sSet the primary modem used for wireless communications.%s [[%sSet the primary modem used for wireless communications.%s
Reboot to take effect.]], Ansi.yellow, Ansi.reset) Reboot to take effect.]], Ansi.yellow, Ansi.reset)
}, },
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
label = UI.Text { label = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Modem', value = 'Modem',
}, },
modem = UI.Chooser { modem = UI.Chooser {
x = 10, ex = -3, y = 2, x = 10, ex = -3, y = 3,
nochoice = 'auto', nochoice = 'auto',
}, },
} enable = function(self)
local width = 7
local choices = {
{ name = 'auto', value = 'auto' },
{ name = 'disable', value = 'none' },
}
function tab:enable() for k,v in pairs(device) do
local width = 7 if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
local choices = { table.insert(choices, { name = k, value = v.name })
{ name = 'auto', value = 'auto' }, width = math.max(width, #k)
{ name = 'disable', value = 'none' }, end
} end
for k,v in pairs(device) do self.modem.choices = choices
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then --self.modem.width = width + 4
table.insert(choices, { name = k, value = v.name })
width = math.max(width, #k) local config = Config.load('os')
self.modem.value = config.wirelessModem or 'auto'
UI.Tab.enable(self)
end,
eventHandler = function(self, event)
if event.type == 'choice_change' then
local config = Config.load('os')
config.wirelessModem = self.modem.value
Config.update('os', config)
self:emit({ type = 'success_message', message = 'reboot to take effect' })
return true
end end
end end
}
self.modem.choices = choices
--self.modem.width = width + 4
local config = Config.load('os')
self.modem.value = config.wirelessModem or 'auto'
UI.Tab.enable(self)
end
function tab:eventHandler(event)
if event.type == 'choice_change' then
local config = Config.load('os')
config.wirelessModem = self.modem.value
Config.update('os', config)
self:emit({ type = 'success_message', message = 'reboot to take effect' })
return true
end
end
return tab

View File

@@ -2,11 +2,12 @@ local Security = require('opus.security')
local SHA = require('opus.crypto.sha2') local SHA = require('opus.crypto.sha2')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors return UI.Tab {
local passwordTab = UI.Tab {
tabTitle = 'Password', tabTitle = 'Password',
description = 'Wireless network password', description = 'Wireless network password',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
newPass = UI.TextEntry { newPass = UI.TextEntry {
x = 3, ex = -3, y = 3, x = 3, ex = -3, y = 3,
limit = 32, limit = 32,
@@ -17,28 +18,28 @@ local passwordTab = UI.Tab {
}, },
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 5, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update_password', event = 'update_password',
}, },
info = UI.TextArea { info = UI.TextArea {
x = 3, ex = -3, y = 7, x = 2, ex = -2, y = 5, ey = -4,
textColor = colors.yellow, backgroundColor = 'black',
textColor = 'yellow',
inactive = true, inactive = true,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Add a password to enable other computers to connect to this one.', value = 'Add a password to enable other computers to connect to this one.',
} },
} eventHandler = function(self, event)
function passwordTab:eventHandler(event) if event.type == 'update_password' then
if event.type == 'update_password' then if not self.newPass.value or #self.newPass.value == 0 then
if not self.newPass.value or #self.newPass.value == 0 then self:emit({ type = 'error_message', message = 'Invalid password' })
self:emit({ type = 'error_message', message = 'Invalid password' })
else else
Security.updatePassword(SHA.compute(self.newPass.value)) Security.updatePassword(SHA.compute(self.newPass.value))
self:emit({ type = 'success_message', message = 'Password updated' }) self:emit({ type = 'success_message', message = 'Password updated' })
end
return true
end end
return true
end end
end }
return passwordTab

View File

@@ -6,8 +6,11 @@ local tab = UI.Tab {
tabTitle = 'Path', tabTitle = 'Path',
description = 'Set the shell path', description = 'Set the shell path',
tabClose = true, tabClose = true,
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
entry = UI.TextEntry { entry = UI.TextEntry {
x = 2, y = 2, ex = -2, x = 3, y = 3, ex = -3,
limit = 256, limit = 256,
shadowText = 'enter new path', shadowText = 'enter new path',
accelerators = { accelerators = {
@@ -16,7 +19,7 @@ local tab = UI.Tab {
help = 'add a new path', help = 'add a new path',
}, },
grid = UI.Grid { grid = UI.Grid {
y = 4, ey = -3, x = 2, y = 6, ex = -2, ey = -3,
disableHeader = true, disableHeader = true,
columns = { { key = 'value' } }, columns = { { key = 'value' } },
autospace = true, autospace = true,
@@ -59,7 +62,7 @@ function tab:save()
end end
function tab:eventHandler(event) function tab:eventHandler(event)
if event.type == 'update_path' then if event.type == 'update_path' and self.entry.value then
table.insert(self.grid.values, { table.insert(self.grid.values, {
value = self.entry.value, value = self.entry.value,
}) })

View File

@@ -2,48 +2,94 @@ local UI = require('opus.ui')
local settings = _G.settings local settings = _G.settings
if settings then local transform = {
local settingsTab = UI.Tab { string = tostring,
tabTitle = 'Settings', number = tonumber,
description = 'Computercraft configurable settings', }
grid = UI.Grid {
y = 2,
autospace = true,
sortColumn = 'name',
columns = {
{ heading = 'Setting', key = 'name' },
{ heading = 'Value', key = 'value' },
},
},
}
function settingsTab:enable() return settings and UI.Tab {
tabTitle = 'Settings',
description = 'Computercraft settings',
grid = UI.Grid {
x = 2, y = 2, ex = -2, ey = -2,
sortColumn = 'name',
columns = {
{ heading = 'Setting', key = 'name' },
{ heading = 'Value', key = 'value' },
},
},
editor = UI.SlideOut {
y = -6, height = 6,
titleBar = UI.TitleBar {
event = 'slide_hide',
title = 'Enter value',
},
form = UI.Form {
y = 2,
value = UI.TextEntry {
limit = 256,
formIndex = 1,
formLabel = 'Value',
formKey = 'value',
},
validateField = function(self, entry)
if entry.value then
return transform[self.type](entry.value)
end
return true
end,
},
accelerators = {
form_cancel = 'slide_hide',
},
show = function(self, entry)
self.form.type = type(entry.value) or 'string'
self.form:setValues(entry)
self.titleBar.title = entry.name
UI.SlideOut.show(self)
end,
eventHandler = function(self, event)
if event.type == 'form_complete' then
if not event.values.value then
settings.unset(event.values.name)
self.parent:reload()
else
event.values.value = transform[self.form.type](event.values.value)
settings.set(event.values.name, event.values.value)
end
self.parent.grid:draw()
self:hide()
settings.save('.settings')
end
return UI.SlideOut.eventHandler(self, event)
end,
},
reload = function(self)
local values = { } local values = { }
for _,v in pairs(settings.getNames()) do for _,v in pairs(settings.getNames()) do
local value = settings.get(v)
if not value then
value = false
end
table.insert(values, { table.insert(values, {
name = v, name = v,
value = value, value = settings.get(v) or false,
}) })
end end
self.grid:setValues(values) self.grid:setValues(values)
self.grid:setIndex(1)
end,
enable = function(self)
self:reload()
UI.Tab.enable(self) UI.Tab.enable(self)
end end,
eventHandler = function(self, event)
function settingsTab:eventHandler(event)
if event.type == 'grid_select' then if event.type == 'grid_select' then
if not event.selected.value or type(event.selected.value) == 'boolean' then if type(event.selected.value) == 'boolean' then
event.selected.value = not event.selected.value event.selected.value = not event.selected.value
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
else
self.editor:show(event.selected)
end end
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
return true return true
end end
end end,
}
return settingsTab
end

View File

@@ -28,7 +28,7 @@ local defaults = {
local _colors = config.color or Util.shallowCopy(defaults) local _colors = config.color or Util.shallowCopy(defaults)
local allSettings = { } local allSettings = { }
for k, v in pairs(defaults) do for k in pairs(defaults) do
table.insert(allSettings, { name = k }) table.insert(allSettings, { name = k })
end end
@@ -38,29 +38,34 @@ if not _colors.backgroundColor then
_colors.fileColor = colors.white _colors.fileColor = colors.white
end end
local tab = UI.Tab { return UI.Tab {
tabTitle = 'Shell', tabTitle = 'Shell',
description = 'Shell options', description = 'Shell options',
grid1 = UI.ScrollingGrid { grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 3, ex = -16, y = 2, ey = -10, x = 2, ex = -17,
disableHeader = true, disableHeader = true,
columns = { { key = 'name' } }, columns = { { key = 'name' } },
values = allSettings, values = allSettings,
sortColumn = 'name', sortColumn = 'name',
}, },
grid2 = UI.ScrollingGrid { grid2 = UI.ScrollingGrid {
y = 2, ey = -10, x = -14, ex = -3, y = 2, ey = -10, x = -14, ex = -2,
disableHeader = true, disableHeader = true,
columns = { { key = 'name' } }, columns = { { key = 'name' } },
values = allColors, values = allColors,
sortColumn = 'name', sortColumn = 'name',
}, getRowTextColor = function(self, row)
directoryLabel = UI.Text { local selected = self.parent.grid1:getSelected()
x = 2, y = -2, if _colors[selected.name] == row.value then
value = 'Display directory', return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
}, },
directory = UI.Checkbox { directory = UI.Checkbox {
x = 20, y = -2, x = 2, y = -2,
labelBackgroundColor = colors.black,
label = 'Directory',
value = config.displayDirectory value = config.displayDirectory
}, },
reset = UI.Button { reset = UI.Button {
@@ -74,69 +79,57 @@ local tab = UI.Tab {
event = 'update', event = 'update',
}, },
display = UI.Window { display = UI.Window {
x = 3, ex = -3, y = -8, height = 5, x = 2, ex = -2, y = -8, height = 5,
draw = function(self)
self:clear(_colors.backgroundColor)
local offset = 0
if config.displayDirectory then
self:write(1, 1,
'==' .. os.getComputerLabel() .. ':/dir/etc',
_colors.directoryBackgroundColor, _colors.directoryTextColor)
offset = 1
end
self:write(1, 1 + offset, '$ ',
_colors.promptBackgroundColor, _colors.promptTextColor)
self:write(3, 1 + offset, 'ls /',
_colors.backgroundColor, _colors.commandTextColor)
self:write(1, 2 + offset, 'sys usr',
_colors.backgroundColor, _colors.directoryColor)
self:write(1, 3 + offset, 'startup',
_colors.backgroundColor, _colors.fileColor)
end,
}, },
eventHandler = function(self, event)
if event.type =='checkbox_change' then
config.displayDirectory = not not event.checked
self.display:draw()
elseif event.type == 'grid_focus_row' and event.element == self.grid1 then
self.grid2:draw()
elseif event.type == 'grid_select' and event.element == self.grid2 then
_colors[self.grid1:getSelected().name] = event.selected.value
self.display:draw()
self.grid2:draw()
elseif event.type == 'reset' then
config.color = defaults
config.displayDirectory = true
self.directory.value = true
_colors = Util.shallowCopy(defaults)
Config.update('shellprompt', config)
self:draw()
elseif event.type == 'update' then
config.color = _colors
Config.update('shellprompt', config)
end
return UI.Tab.eventHandler(self, event)
end
} }
function tab.grid2:getRowTextColor(row)
local selected = tab.grid1:getSelected()
if _colors[selected.name] == row.value then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
function tab.display:draw()
self:clear(_colors.backgroundColor)
local offset = 0
if config.displayDirectory then
self:write(1, 1,
'==' .. os.getComputerLabel() .. ':/dir/etc',
_colors.directoryBackgroundColor, _colors.directoryTextColor)
offset = 1
end
self:write(1, 1 + offset, '$ ',
_colors.promptBackgroundColor, _colors.promptTextColor)
self:write(3, 1 + offset, 'ls /',
_colors.backgroundColor, _colors.commandTextColor)
self:write(1, 2 + offset, 'sys usr',
_colors.backgroundColor, _colors.directoryColor)
self:write(1, 3 + offset, 'startup',
_colors.backgroundColor, _colors.fileColor)
end
function tab:eventHandler(event)
if event.type =='checkbox_change' then
config.displayDirectory = not not event.checked
self.display:draw()
elseif event.type == 'grid_focus_row' and event.element == self.grid1 then
self.grid2:draw()
elseif event.type == 'grid_select' and event.element == self.grid2 then
_colors[tab.grid1:getSelected().name] = event.selected.value
self.display:draw()
self.grid2:draw()
elseif event.type == 'reset' then
config.color = defaults
config.displayDirectory = true
self.directory.value = true
_colors = Util.shallowCopy(defaults)
Config.update('shellprompt', config)
self:draw()
elseif event.type == 'update' then
config.color = _colors
Config.update('shellprompt', config)
end
return UI.Tab.eventHandler(self, event)
end
return tab

89
sys/apps/system/theme.lua Normal file
View File

@@ -0,0 +1,89 @@
local Config = require('opus.config')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local allColors = { }
for k,v in pairs(colors) do
if type(v) == 'number' then
table.insert(allColors, { name = k, value = v })
end
end
local allSettings = { }
for k,v in pairs(UI.colors) do
allSettings[k] = { name = k, value = v }
end
return UI.Tab {
tabTitle = 'Theme',
description = 'Theme colors',
grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 2, ex = -17,
disableHeader = true,
columns = { { key = 'name' } },
values = allSettings,
sortColumn = 'name',
},
grid2 = UI.ScrollingGrid {
y = 2, ey = -10, x = -14, ex = -2,
disableHeader = true,
columns = { { key = 'name' } },
values = allColors,
sortColumn = 'name',
getRowTextColor = function(self, row)
local selected = self.parent.grid1:getSelected()
if selected.value == row.value then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
},
button = UI.Button {
x = -9, y = -2,
text = 'Update',
event = 'update',
},
display = UI.Window {
x = 2, ex = -2, y = -8, height = 5,
textColor = colors.black,
backgroundColor = colors.black,
draw = function(self)
self:clear()
self:write(1, 1, Util.widthify(' Local Global Device', self.width),
allSettings.secondary.value)
self:write(2, 2, 'enter command ',
colors.black, colors.gray)
self:write(1, 3, ' Formatted ',
allSettings.primary.value)
self:write(12, 3, Util.widthify(' Output ', self.width - 11),
allSettings.tertiary.value)
self:write(1, 4, Util.widthify(' Key', self.width),
allSettings.primary.value)
end,
},
eventHandler = function(self, event)
if event.type == 'grid_focus_row' and event.element == self.grid1 then
self.grid2:draw()
elseif event.type == 'grid_select' and event.element == self.grid2 then
self.grid1:getSelected().value = event.selected.value
self.display:draw()
self.grid2:draw()
elseif event.type == 'update' then
local config = Config.load('ui.theme', { colors = { } })
for k,v in pairs(allSettings) do
config.colors[k] = v.value
end
Config.update('ui.theme', config)
end
return UI.Tab.eventHandler(self, event)
end
}

View File

@@ -9,7 +9,7 @@ kernel.hook('clipboard_copy', function(_, args)
keyboard.clipboard = args[1] keyboard.clipboard = args[1]
end) end)
keyboard.addHotkey('shift-paste', function() local function queuePaste()
local data = keyboard.clipboard local data = keyboard.clipboard
if type(data) == 'table' then if type(data) == 'table' then
@@ -20,4 +20,7 @@ keyboard.addHotkey('shift-paste', function()
if data then if data then
os.queueEvent('paste', data) os.queueEvent('paste', data)
end end
end) end
kernel.hook('clipboard_paste', queuePaste)
keyboard.addHotkey('shift-paste', queuePaste)

View File

@@ -1,3 +1,5 @@
local fs = _G.fs
local function completeMultipleChoice(sText, tOptions, bAddSpaces) local function completeMultipleChoice(sText, tOptions, bAddSpaces)
local tResults = { } local tResults = { }
for n = 1,#tOptions do for n = 1,#tOptions do
@@ -20,3 +22,14 @@ _ENV.shell.setCompletionFunction("sys/apps/package.lua",
return completeMultipleChoice(text, { "install ", "update ", "uninstall ", "updateall ", "refresh" }) return completeMultipleChoice(text, { "install ", "update ", "uninstall ", "updateall ", "refresh" })
end end
end) end)
_ENV.shell.setCompletionFunction("sys/apps/inspect.lua",
function(_, index, text)
if index == 1 then
local components = { }
for _, f in pairs(fs.list('sys/modules/opus/ui/components')) do
table.insert(components, (f:gsub("%.lua$", "")))
end
return completeMultipleChoice(text, components)
end
end)

View File

@@ -4,58 +4,45 @@ local kernel = _G.kernel
local keyboard = _G.device.keyboard local keyboard = _G.device.keyboard
local multishell = _ENV.multishell local multishell = _ENV.multishell
if not multishell or not multishell.getTabs then if multishell and multishell.getTabs then
return -- restart tab
end keyboard.addHotkey('control-backspace', function()
local tab = kernel.getFocused()
-- overview if tab and not tab.noTerminate then
keyboard.addHotkey('control-o', function() multishell.terminate(tab.uid)
for _,tab in pairs(multishell.getTabs()) do multishell.openTab({
if tab.isOverview then path = tab.path,
multishell.setFocus(tab.uid) env = tab.env,
args = tab.args,
focused = true,
})
end end
end end)
end) end
-- restart tab
keyboard.addHotkey('control-backspace', function()
local uid = multishell.getFocus()
local tab = kernel.find(uid)
if not tab.isOverview then
multishell.terminate(uid)
multishell.openTab({
path = tab.path,
env = tab.env,
args = tab.args,
focused = true,
})
end
end)
-- next tab -- next tab
keyboard.addHotkey('control-tab', function() keyboard.addHotkey('control-tab', function()
local tabs = multishell.getTabs()
local visibleTabs = { } local visibleTabs = { }
local currentTabId = multishell.getFocus() local currentTab = kernel.getFocused()
local function compareTab(a, b) local function compareTab(a, b)
return a.uid < b.uid return a.uid < b.uid
end end
for _,tab in Util.spairs(tabs, compareTab) do for _,tab in Util.spairs(kernel.routines, compareTab) do
if not tab.hidden and not tab.noFocus then if not tab.hidden and not tab.noFocus then
table.insert(visibleTabs, tab) table.insert(visibleTabs, tab)
end end
end end
for k,tab in ipairs(visibleTabs) do for k,tab in ipairs(visibleTabs) do
if tab.uid == currentTabId then if tab.uid == currentTab.uid then
if k < #visibleTabs then if k < #visibleTabs then
multishell.setFocus(visibleTabs[k + 1].uid) kernel.raise(visibleTabs[k + 1].uid)
return return
end end
end end
end end
if #visibleTabs > 0 then if #visibleTabs > 0 then
multishell.setFocus(visibleTabs[1].uid) kernel.raise(visibleTabs[1].uid)
end end
end) end)

View File

@@ -50,16 +50,9 @@ local function systemLog()
keyboard.removeHotkey('control-d') keyboard.removeHotkey('control-d')
end end
if multishell and multishell.openTab then kernel.run({
multishell.openTab({ title = 'System Log',
title = 'System Log', fn = systemLog,
fn = systemLog, noTerminate = true,
noTerminate = true, hidden = true,
hidden = true, })
})
else
kernel.run({
title = 'Syslog',
fn = systemLog,
})
end

View File

@@ -1,10 +1,10 @@
local fs = _G.fs local fs = _G.fs
local function deleteIfExists(path) local function deleteIfExists(path)
if fs.exists(path) then if fs.exists(path) then
fs.delete(path) fs.delete(path)
print("Deleted outdated file at: "..path) print("Deleted outdated file at: "..path)
end end
end end
-- cleanup outdated files -- cleanup outdated files
deleteIfExists('sys/apps/shell') deleteIfExists('sys/apps/shell')
@@ -18,3 +18,7 @@ deleteIfExists('sys/autorun/gpshost.lua')
deleteIfExists('sys/apps/network/redserver.lua') deleteIfExists('sys/apps/network/redserver.lua')
deleteIfExists('sys/apis') deleteIfExists('sys/apis')
deleteIfExists('sys/autorun/apps.lua') deleteIfExists('sys/autorun/apps.lua')
deleteIfExists('sys/init/6.tl3.lua')
-- remove this file
-- deleteIfExists('sys/autorun/upgraded.lua')

View File

@@ -50,7 +50,10 @@
title = "System", title = "System",
category = "System", category = "System",
icon = "\030 \0307\031f| \010\0307\031f---o\030 \031 \010\030 \009 \0307\031f| ", icon = "\030 \0307\031f| \010\0307\031f---o\030 \031 \010\030 \009 \0307\031f| ",
iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133", iconExt = "22€070†b02‹4Ÿ24\
02—7Ž704ˆ4€€€€\
7ƒ07„1ƒ7‹24ƒƒ",
--iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133",
run = "System.lua", run = "System.lua",
}, },
[ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = { [ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = {

View File

@@ -6,10 +6,11 @@ Shortcut keys
* l: Lua application * l: Lua application
* f: Files * f: Files
* e: Edit an application (or right-click) * e: Edit an application (or right-click)
* n: Network
* control-n: Add a new application * control-n: Add a new application
* delete: Delete an application * delete: Delete an application
Adding a new application Adding a new application
======================== ========================
The run entry can be either a disk file or a URL. The run entry can be either a disk file or a URL.
Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Magenta is used for transparency.

View File

@@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Peripheral = require('opus.peripheral') local Peripheral = require('opus.peripheral')
_G.device = Peripheral.getList() _G.device = Peripheral.getList()

View File

@@ -4,11 +4,8 @@ if fs.native then
return return
end end
_G.requireInjector(_ENV)
local Util = require('opus.util') local Util = require('opus.util')
-- TODO: support getDrive for virtual nodes
fs.native = Util.shallowCopy(fs) fs.native = Util.shallowCopy(fs)
local fstypes = { } local fstypes = { }
@@ -23,7 +20,6 @@ for k,fn in pairs(fs) do
end end
function nativefs.list(node, dir) function nativefs.list(node, dir)
local files local files
if fs.native.isDir(dir) then if fs.native.isDir(dir) then
files = fs.native.list(dir) files = fs.native.list(dir)
@@ -265,7 +261,6 @@ local function getfstype(fstype)
end end
function fs.mount(path, fstype, ...) function fs.mount(path, fstype, ...)
local vfs = getfstype(fstype) local vfs = getfstype(fstype)
if not vfs then if not vfs then
error('Invalid file system type') error('Invalid file system type')

View File

@@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Config = require('opus.config') local Config = require('opus.config')
local device = _G.device local device = _G.device

View File

@@ -32,3 +32,23 @@ end
help.setPath(table.concat(helpPaths, ':')) help.setPath(table.concat(helpPaths, ':'))
shell.setPath(table.concat(appPaths, ':')) shell.setPath(table.concat(appPaths, ':'))
local function runDir(directory)
local files = fs.list(directory)
table.sort(files)
for _,file in ipairs(files) do
os.sleep(0)
local result, err = shell.run(directory .. '/' .. file)
if not result and err then
_G.printError('\n' .. err)
end
end
end
for _, package in pairs(Packages:installedSorted()) do
local packageDir = 'packages/' .. package.name .. '/init'
if fs.exists(packageDir) and fs.isDir(packageDir) then
runDir(packageDir)
end
end

View File

@@ -1,5 +1,4 @@
_G.requireInjector(_ENV) local Blit = require('opus.ui.blit')
local Config = require('opus.config') local Config = require('opus.config')
local trace = require('opus.trace') local trace = require('opus.trace')
local Util = require('opus.util') local Util = require('opus.util')
@@ -47,6 +46,7 @@ local config = {
Config.load('multishell', config) Config.load('multishell', config)
local _colors = parentTerm.isColor() and config.color or config.standard local _colors = parentTerm.isColor() and config.color or config.standard
local palette = parentTerm.isColor() and Blit.colorPalette or Blit.grayscalePalette
local function redrawMenu() local function redrawMenu()
if not tabsDirty then if not tabsDirty then
@@ -207,17 +207,11 @@ end)
kernel.hook('multishell_redraw', function() kernel.hook('multishell_redraw', function()
tabsDirty = false tabsDirty = false
local function write(x, text, bg, fg) local blit = Blit(w, {
parentTerm.setBackgroundColor(bg) bg = _colors.tabBarBackgroundColor,
parentTerm.setTextColor(fg) fg = _colors.textColor,
parentTerm.setCursorPos(x, 1) palette = palette,
parentTerm.write(text) })
end
local bg = _colors.tabBarBackgroundColor
parentTerm.setBackgroundColor(bg)
parentTerm.setCursorPos(1, 1)
parentTerm.clearLine()
local currentTab = kernel.getFocused() local currentTab = kernel.getFocused()
@@ -254,21 +248,26 @@ kernel.hook('multishell_redraw', function()
tabX = tabX + tab.width tabX = tabX + tab.width
if tab ~= currentTab then if tab ~= currentTab then
local textColor = tab.isDead and _colors.errorColor or _colors.textColor local textColor = tab.isDead and _colors.errorColor or _colors.textColor
write(tab.sx, tab.title:sub(1, tab.width - 1), blit:write(tab.sx, tab.title:sub(1, tab.width - 1),
_colors.backgroundColor, textColor) _colors.backgroundColor, textColor)
end end
end end
end end
if currentTab then if currentTab then
write(currentTab.sx - 1, if currentTab.sx then
' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ', blit:write(currentTab.sx - 1,
_colors.focusBackgroundColor, _colors.focusTextColor) ' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ',
_colors.focusBackgroundColor, _colors.focusTextColor)
end
if not currentTab.noTerminate then if not currentTab.noTerminate then
write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor) blit:write(w, closeInd, nil, _colors.focusTextColor)
end end
end end
parentTerm.setCursorPos(1, 1)
parentTerm.blit(blit.text, blit.fg, blit.bg)
if currentTab and currentTab.window then if currentTab and currentTab.window then
currentTab.window.restoreCursor() currentTab.window.restoreCursor()
end end
@@ -334,16 +333,12 @@ kernel.hook('mouse_scroll', function(_, eventData)
end) end)
kernel.hook('kernel_ready', function() kernel.hook('kernel_ready', function()
local env = Util.shallowCopy(shell.getEnv())
_G.requireInjector(env)
overviewId = multishell.openTab({ overviewId = multishell.openTab({
path = config.launcher or 'sys/apps/Overview.lua', path = config.launcher or 'sys/apps/Overview.lua',
isOverview = true, isOverview = true,
noTerminate = true, noTerminate = true,
focused = true, focused = true,
title = '+', title = '+',
env = env,
}) })
multishell.openTab({ multishell.openTab({

View File

@@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Array = require('opus.array') local Array = require('opus.array')
local Terminal = require('opus.terminal') local Terminal = require('opus.terminal')
local Util = require('opus.util') local Util = require('opus.util')

View File

@@ -1,3 +1,5 @@
local Util = require('opus.util')
local Array = { } local Array = { }
function Array.filter(it, f) function Array.filter(it, f)
@@ -14,9 +16,11 @@ function Array.removeByValue(t, e)
for k,v in pairs(t) do for k,v in pairs(t) do
if v == e then if v == e then
table.remove(t, k) table.remove(t, k)
break return e
end end
end end
end end
Array.find = Util.find
return Array return Array

View File

@@ -1,7 +1,6 @@
local Util = require('opus.util') local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local shell = _ENV.shell
local Config = { } local Config = { }
@@ -25,23 +24,6 @@ function Config.load(fname, data)
return data return data
end end
function Config.loadWithCheck(fname, data)
local filename = 'usr/config/' .. fname
if not fs.exists(filename) then
Config.load(fname, data)
print()
print('The configuration file has been created.')
print('The file name is: ' .. filename)
print()
_G.printError('Press enter to configure')
_G.read()
shell.run('edit ' .. filename)
end
return Config.load(fname, data)
end
function Config.update(fname, data) function Config.update(fname, data)
local filename = 'usr/config/' .. fname local filename = 'usr/config/' .. fname
Util.writeTable(filename, data) Util.writeTable(filename, data)

View File

@@ -12,12 +12,11 @@ local band = bit32.band
local blshift = bit32.lshift local blshift = bit32.lshift
local brshift = bit32.arshift local brshift = bit32.arshift
local textutils = _G.textutils local textutils = _G.textutils
local mt = Util.byteArrayMT
local mod = 2^32 local mod = 2^32
local tau = {("expand 16-byte k"):byte(1,-1)} local tau = {("expand 16-byte k"):byte(1,-1)}
local sigma = {("expand 32-byte k"):byte(1,-1)} local sigma = {("expand 32-byte k"):byte(1,-1)}
local null32 = {("A"):rep(32):byte(1,-1)}
local null12 = {("A"):rep(12):byte(1,-1)}
local function rotl(n, b) local function rotl(n, b)
local s = n/(2^(32-b)) local s = n/(2^(32-b))
@@ -91,22 +90,6 @@ local function serialize(state)
return r return r
end end
local mt = {
__tostring = function(a) return string.char(table.unpack(a)) end,
__index = {
toHex = function(self) return ("%02x"):rep(#self):format(table.unpack(self)) end,
isEqual = function(self, t)
if type(t) ~= "table" then return false end
if #self ~= #t then return false end
local ret = 0
for i = 1, #self do
ret = bit32.bor(ret, bxor(self[i], t[i]))
end
return ret == 0
end
}
}
local function crypt(data, key, nonce, cntr, round) local function crypt(data, key, nonce, cntr, round)
assert(type(key) == "table", "ChaCha20: Invalid key format ("..type(key).."), must be table") assert(type(key) == "table", "ChaCha20: Invalid key format ("..type(key).."), must be table")
assert(type(nonce) == "table", "ChaCha20: Invalid nonce format ("..type(nonce).."), must be table") assert(type(nonce) == "table", "ChaCha20: Invalid nonce format ("..type(nonce).."), must be table")
@@ -133,15 +116,12 @@ local function crypt(data, key, nonce, cntr, round)
out[#out+1] = bxor(block[j], ks[j]) out[#out+1] = bxor(block[j], ks[j])
end end
--if i % 1000 == 0 then throttle()
throttle()
--os.queueEvent("")
--os.pullEvent("")
--end
end end
return setmetatable(out, mt) return setmetatable(out, mt)
end end
-- Helper functions
local function genNonce(len) local function genNonce(len)
local nonce = {} local nonce = {}
for i = 1, len do for i = 1, len do
@@ -170,6 +150,9 @@ end
local obj = {} local obj = {}
local rng_mt = {['__index'] = obj} local rng_mt = {['__index'] = obj}
-- PRNG object
local null32 = {("A"):rep(32):byte(1,-1)}
local null12 = {("A"):rep(12):byte(1,-1)}
function obj:nextInt(byte) function obj:nextInt(byte)
if not byte or byte < 1 or byte > 6 then error("Can only return 1-6 bytes", 2) end if not byte or byte < 1 or byte > 6 then error("Can only return 1-6 bytes", 2) end
local output = 0 local output = 0

View File

@@ -1,9 +1,12 @@
local fq = require('opus.crypto.ecc.fq') local fq = require('opus.crypto.ecc.fq')
local elliptic = require('opus.crypto.ecc.elliptic') local elliptic = require('opus.crypto.ecc.elliptic')
local sha256 = require('opus.crypto.sha2') local sha256 = require('opus.crypto.sha2')
local Util = require('opus.util')
local os = _G.os local os = _G.os
local unpack = table.unpack local unpack = table.unpack
local mt = Util.byteArrayMT
local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532} local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532}
@@ -27,7 +30,7 @@ local function publicKey(sk)
local Y = elliptic.scalarMulG(x) local Y = elliptic.scalarMulG(x)
local pk = elliptic.pointEncode(Y) local pk = elliptic.pointEncode(Y)
return pk return setmetatable(pk, mt)
end end
local function exchange(sk, pk) local function exchange(sk, pk)
@@ -62,7 +65,7 @@ local function sign(sk, message)
sig[#sig + 1] = s[i] sig[#sig + 1] = s[i]
end end
return sig return setmetatable(sig, mt)
end end
local function verify(pk, message, sig) local function verify(pk, message, sig)

View File

@@ -9,6 +9,7 @@ local bnot = bit32 and bit32.bnot or bit.bnot
local bxor = bit32 and bit32.bxor or bit.bxor local bxor = bit32 and bit32.bxor or bit.bxor
local blshift = bit32 and bit32.lshift or bit.blshift local blshift = bit32 and bit32.lshift or bit.blshift
local upack = unpack or table.unpack local upack = unpack or table.unpack
local mt = Util.byteArrayMT
local function rrotate(n, b) local function rrotate(n, b)
local s = n/(2^b) local s = n/(2^b)
@@ -68,17 +69,16 @@ end
local function digestblock(w, C) local function digestblock(w, C)
for j = 17, 64 do for j = 17, 64 do
-- local v = w[j-15] local s0 = bxor(rrotate(w[j-15], 7), rrotate(w[j-15], 18), brshift(w[j-15], 3))
local s0 = bxor(bxor(rrotate(w[j-15], 7), rrotate(w[j-15], 18)), brshift(w[j-15], 3)) local s1 = bxor(rrotate(w[j-2], 17), rrotate(w[j-2], 19), brshift(w[j-2], 10))
local s1 = bxor(bxor(rrotate(w[j-2], 17), rrotate(w[j-2], 19)), brshift(w[j-2], 10))
w[j] = (w[j-16] + s0 + w[j-7] + s1)%mod32 w[j] = (w[j-16] + s0 + w[j-7] + s1)%mod32
end end
local a, b, c, d, e, f, g, h = upack(C) local a, b, c, d, e, f, g, h = upack(C)
for j = 1, 64 do for j = 1, 64 do
local S1 = bxor(bxor(rrotate(e, 6), rrotate(e, 11)), rrotate(e, 25)) local S1 = bxor(rrotate(e, 6), rrotate(e, 11), rrotate(e, 25))
local ch = bxor(band(e, f), band(bnot(e), g)) local ch = bxor(band(e, f), band(bnot(e), g))
local temp1 = (h + S1 + ch + K[j] + w[j])%mod32 local temp1 = (h + S1 + ch + K[j] + w[j])%mod32
local S0 = bxor(bxor(rrotate(a, 2), rrotate(a, 13)), rrotate(a, 22)) local S0 = bxor(rrotate(a, 2), rrotate(a, 13), rrotate(a, 22))
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c)) local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
local temp2 = (S0 + maj)%mod32 local temp2 = (S0 + maj)%mod32
h, g, f, e, d, c, b, a = g, f, e, (d+temp1)%mod32, c, b, a, (temp1+temp2)%mod32 h, g, f, e, d, c, b, a = g, f, e, (d+temp1)%mod32, c, b, a, (temp1+temp2)%mod32
@@ -94,22 +94,6 @@ local function digestblock(w, C)
return C return C
end end
local mt = {
__tostring = function(a) return string.char(upack(a)) end,
__index = {
toHex = function(self) return ("%02x"):rep(#self):format(upack(self)) end,
isEqual = function(self, t)
if type(t) ~= "table" then return false end
if #self ~= #t then return false end
local ret = 0
for i = 1, #self do
ret = bit32.bor(ret, bxor(self[i], t[i]))
end
return ret == 0
end
}
}
local function toBytes(t, n) local function toBytes(t, n)
local b = {} local b = {}
for i = 1, n do for i = 1, n do

View File

@@ -2,39 +2,48 @@ local class = require('opus.class')
local os = _G.os local os = _G.os
-- convert value to a string (supporting nils or numbers in value)
local function _val(a)
return a and tostring(a) or ''
end
local Entry = class() local Entry = class()
function Entry:init(args) function Entry:init(args)
self.pos = 0 self.pos = 0
self.scroll = 0 self.scroll = 0
self.value = '' self.value = args.value
self.width = args.width or 256 self.width = args.width or 256
self.limit = args.limit or 1024 self.limit = args.limit or 1024
self.mark = { } self.mark = { }
self.offset = args.offset or 1 self.offset = args.offset or 1
self.transform = args.transform or function(a) return a end
end end
function Entry:reset() function Entry:reset()
self.pos = 0 self.pos = 0
self.scroll = 0 self.scroll = 0
self.value = '' self.value = nil
self.mark = { } self.mark = { }
end end
function Entry:nextWord() function Entry:nextWord()
return select(2, self.value:find("[%s%p]?%w[%s%p]", self.pos + 1)) or #self.value local value = _val(self.value)
return select(2, value:find("[%s%p]?%w[%s%p]", self.pos + 1)) or #value
end end
function Entry:prevWord() function Entry:prevWord()
local x = #self.value - (self.pos - 1) local value = _val(self.value)
local _, n = self.value:reverse():find("[%s%p]?%w[%s%p]", x) local x = #value - (self.pos - 1)
return n and #self.value - n + 1 or 0 local _, n = value:reverse():find("[%s%p]?%w[%s%p]", x)
return n and #value - n + 1 or 0
end end
function Entry:updateScroll() function Entry:updateScroll()
local ps = self.scroll local ps = self.scroll
if self.pos > #self.value then local len = #_val(self.value)
self.pos = #self.value if self.pos > len then
self.pos = len
self.scroll = 0 -- ?? self.scroll = 0 -- ??
end end
if self.pos - self.scroll > self.width then if self.pos - self.scroll > self.width then
@@ -42,27 +51,38 @@ function Entry:updateScroll()
elseif self.pos < self.scroll then elseif self.pos < self.scroll then
self.scroll = self.pos self.scroll = self.pos
end end
if self.scroll > 0 then
if self.scroll + self.width > len then
self.scroll = len - self.width
end
end
if ps ~= self.scroll then if ps ~= self.scroll then
self.textChanged = true self.textChanged = true
end end
end end
function Entry:copyText(cx, ex) function Entry:copyText(cx, ex)
return self.value:sub(cx + 1, ex) -- this should be transformed (ie. if number)
return _val(self.value):sub(cx + 1, ex)
end end
function Entry:insertText(x, text) function Entry:insertText(x, text)
if #self.value + #text > self.limit then text = tostring(self.transform(text) or '')
text = text:sub(1, self.limit-#self.value) if #text > 0 then
local value = _val(self.value)
if #value + #text > self.limit then
text = text:sub(1, self.limit-#value)
end
self.value = self.transform(value:sub(1, x) .. text .. value:sub(x + 1))
self.pos = self.pos + #text
end end
self.value = self.value:sub(1, x) .. text .. self.value:sub(x + 1)
self.pos = self.pos + #text
end end
function Entry:deleteText(sx, ex) function Entry:deleteText(sx, ex)
local front = self.value:sub(1, sx) local value = _val(self.value)
local back = self.value:sub(ex + 1, #self.value) local front = value:sub(1, sx)
self.value = front .. back local back = value:sub(ex + 1, #value)
self.value = self.transform(front .. back)
self.pos = sx self.pos = sx
end end
@@ -74,7 +94,7 @@ function Entry:moveLeft()
end end
function Entry:moveRight() function Entry:moveRight()
if self.pos < #self.value then if self.pos < #_val(self.value) then
self.pos = self.pos + 1 self.pos = self.pos + 1
return true return true
end end
@@ -88,14 +108,14 @@ function Entry:moveHome()
end end
function Entry:moveEnd() function Entry:moveEnd()
if self.pos ~= #self.value then if self.pos ~= #_val(self.value) then
self.pos = #self.value self.pos = #_val(self.value)
return true return true
end end
end end
function Entry:moveTo(ie) function Entry:moveTo(ie)
self.pos = math.max(0, math.min(ie.x + self.scroll - self.offset, #self.value)) self.pos = math.max(0, math.min(ie.x + self.scroll - self.offset, #_val(self.value)))
end end
function Entry:backspace() function Entry:backspace()
@@ -107,7 +127,7 @@ function Entry:backspace()
end end
function Entry:moveWordRight() function Entry:moveWordRight()
if self.pos < #self.value then if self.pos < #_val(self.value) then
self.pos = self:nextWord(self.value, self.pos + 1) self.pos = self:nextWord(self.value, self.pos + 1)
return true return true
end end
@@ -123,7 +143,7 @@ end
function Entry:delete() function Entry:delete()
if self.mark.active then if self.mark.active then
self:deleteText(self.mark.x, self.mark.ex) self:deleteText(self.mark.x, self.mark.ex)
elseif self.pos < #self.value then elseif self.pos < #_val(self.value) then
self:deleteText(self.pos, self.pos + 1) self:deleteText(self.pos, self.pos + 1)
end end
end end
@@ -137,15 +157,16 @@ function Entry:cutFromStart()
end end
function Entry:cutToEnd() function Entry:cutToEnd()
if self.pos < #self.value then local value = _val(self.value)
local text = self:copyText(self.pos, #self.value) if self.pos < #value then
self:deleteText(self.pos, #self.value) local text = self:copyText(self.pos, #value)
self:deleteText(self.pos, #value)
os.queueEvent('clipboard_copy', text) os.queueEvent('clipboard_copy', text)
end end
end end
function Entry:cutNextWord() function Entry:cutNextWord()
if self.pos < #self.value then if self.pos < #_val(self.value) then
local ex = self:nextWord(self.value, self.pos) local ex = self:nextWord(self.value, self.pos)
local text = self:copyText(self.pos, ex) local text = self:copyText(self.pos, ex)
self:deleteText(self.pos, ex) self:deleteText(self.pos, ex)
@@ -170,7 +191,7 @@ function Entry:insertChar(ie)
end end
function Entry:copy() function Entry:copy()
if #self.value > 0 then if #_val(self.value) > 0 then
self.mark.continue = true self.mark.continue = true
if self.mark.active then if self.mark.active then
self:copyMarked() self:copyMarked()
@@ -201,8 +222,12 @@ function Entry:paste(ie)
end end
end end
function Entry:forcePaste()
os.queueEvent('clipboard_paste')
end
function Entry:clearLine() function Entry:clearLine()
if #self.value > 0 then if #_val(self.value) > 0 then
self:reset() self:reset()
end end
end end
@@ -233,10 +258,13 @@ function Entry:unmark()
end end
function Entry:markAnchor(ie) function Entry:markAnchor(ie)
local wasMarking = self.mark.active
self:unmark() self:unmark()
self:moveTo(ie) self:moveTo(ie)
self:markBegin() self:markBegin()
self:markFinish() self:markFinish()
self.textChanged = wasMarking
end end
function Entry:markLeft() function Entry:markLeft()
@@ -257,7 +285,7 @@ function Entry:markWord(ie)
local index = 1 local index = 1
self:moveTo(ie) self:moveTo(ie)
while true do while true do
local s, e = self.value:find('%w+', index) local s, e = _val(self.value):find('%w+', index)
if not s or s - 1 > self.pos then if not s or s - 1 > self.pos then
break break
end end
@@ -288,12 +316,12 @@ function Entry:markPrevWord()
end end
function Entry:markAll() function Entry:markAll()
if #self.value > 0 then if #_val(self.value) > 0 then
self.mark.anchor = { x = 1 } self.mark.anchor = { x = 1 }
self.mark.active = true self.mark.active = true
self.mark.continue = true self.mark.continue = true
self.mark.x = 0 self.mark.x = 0
self.mark.ex = #self.value self.mark.ex = #_val(self.value)
self.textChanged = true self.textChanged = true
end end
end end
@@ -344,9 +372,10 @@ local mappings = {
--[ 'control-d' ] = Entry.cutNextWord, --[ 'control-d' ] = Entry.cutNextWord,
[ 'control-x' ] = Entry.cut, [ 'control-x' ] = Entry.cut,
[ 'paste' ] = Entry.paste, [ 'paste' ] = Entry.paste,
-- [ 'control-y' ] = Entry.paste, -- well this won't work... [ 'control-y' ] = Entry.forcePaste, -- well this won't work...
[ 'mouse_doubleclick' ] = Entry.markWord, [ 'mouse_doubleclick' ] = Entry.markWord,
[ 'mouse_tripleclick' ] = Entry.markAll,
[ 'shift-left' ] = Entry.markLeft, [ 'shift-left' ] = Entry.markLeft,
[ 'shift-right' ] = Entry.markRight, [ 'shift-right' ] = Entry.markRight,
[ 'mouse_down' ] = Entry.markAnchor, [ 'mouse_down' ] = Entry.markAnchor,
@@ -373,6 +402,10 @@ function Entry:process(ie)
action(self, ie) action(self, ie)
if not self.value or #_val(self.value) == 0 then
self.value = nil
end
self.textChanged = self.textChanged or self.value ~= line self.textChanged = self.textChanged or self.value ~= line
self.posChanged = pos ~= self.pos self.posChanged = pos ~= self.pos
self:updateScroll() self:updateScroll()

View File

@@ -77,6 +77,10 @@ function ramfs.open(node, fn, fl)
local ctr = 0 local ctr = 0
local lines local lines
return { return {
read = function()
ctr = ctr + 1
return node.contents:sub(ctr, ctr)
end,
readLine = function() readLine = function()
if not lines then if not lines then
lines = Util.split(node.contents) lines = Util.split(node.contents)

View File

@@ -74,6 +74,10 @@ function urlfs.open(node, fn, fl)
if fl == 'r' then if fl == 'r' then
return { return {
read = function()
ctr = ctr + 1
return c:sub(ctr, ctr)
end,
readLine = function() readLine = function()
if not lines then if not lines then
lines = Util.split(c) lines = Util.split(c)

View File

@@ -0,0 +1,21 @@
-- Based on Squid's fuzzy search
-- https://github.com/SquidDev-CC/artist/blob/vnext/artist/lib/match.lua
--
-- not very fuzzy anymore
local SCORE_WEIGHT = 1000
local LEADING_LETTER_PENALTY = -30
local LEADING_LETTER_PENALTY_MAX = -90
local _find = string.find
local _max = math.max
return function(str, pattern)
local start = _find(str, pattern, 1, true)
if start then
-- All letters before the current one are considered leading, so add them to our penalty
return SCORE_WEIGHT
+ _max(LEADING_LETTER_PENALTY * (start - 1), LEADING_LETTER_PENALTY_MAX)
- (#str - #pattern)
end
end

View File

@@ -3,10 +3,11 @@ local Util = require('opus.util')
local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1' local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s' local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
local TREE_HEADERS = {}
local git = { } local git = { }
if _G._GIT_API_KEY then if _G._GIT_API_KEY then
TREE_URL = TREE_URL .. '&access_token=' .. _G._GIT_API_KEY TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY
end end
function git.list(repository) function git.list(repository)
@@ -23,8 +24,10 @@ function git.list(repository)
local function getContents() local function getContents()
local dataUrl = string.format(TREE_URL, user, repo, branch) local dataUrl = string.format(TREE_URL, user, repo, branch)
local contents = Util.download(dataUrl) local contents, msg = Util.httpGet(dataUrl, TREE_HEADERS)
if contents then if not contents then
error(string.format('Failed to download %s\n%s', dataUrl, msg), 2)
else
return json.decode(contents) return json.decode(contents)
end end
end end

View File

@@ -1,17 +1,71 @@
local Util = require('opus.util') local Util = require('opus.util')
local GPS = { } local GPS = { }
GPS.CHANNEL_GPS = 65534
local device = _G.device local device = _G.device
local gps = _G.gps local vector = _G.vector
function GPS.locate(timeout, debug) function GPS.locate(timeout, debug)
local pt = { } if not device.wireless_modem then
timeout = timeout or 10 if debug then
pt.x, pt.y, pt.z = gps.locate(timeout, debug) print('No wireless modem attached')
if pt.x then end
return pt return nil
end end
if debug then
print('Finding position...')
end
local modem = device.wireless_modem
local closeChannel = false
local selfID = os.getComputerID()
if not modem.isOpen(selfID) then
modem.open(selfID)
closeChannel = true
end
modem.transmit(GPS.CHANNEL_GPS, selfID, "PING")
local fixes = {}
local pos = nil
local timer = os.startTimer(timeout or 1)
while true do
local e, side, chan, reply, msg, dist = os.pullEvent()
if e == "modem_message" then
if side == modem.side and chan == selfID and reply == GPS.CHANNEL_GPS and dist then
if type(msg) == "table" and #msg == 3 and tonumber(msg[1]) and tonumber(msg[2]) and tonumber(msg[3]) then
local fix = {
position = vector.new(unpack(msg)),
distance = dist,
}
if debug then
print(fix.distance..' meters from '..fix.position:tostring())
end
if fix.distance == 0 then
pos = fix.position
else
fixes[#fixes+1] = fix
if #fixes > 3 then
pos = GPS.trilaterate(fixes)
if pos then break end
end
end
end
end
elseif e == "timer" and side == timer then
break
end
end
if closeChannel then
modem.close(selfID)
end
if debug then
print("Position is "..pos.x..","..pos.y..","..pos.z)
end
return pos and vector.new(pos.x, pos.y, pos.z)
end end
function GPS.isAvailable() function GPS.isAvailable()
@@ -66,26 +120,26 @@ local function trilaterate(A, B, C)
local result1 = result + (ez * z) local result1 = result + (ez * z)
local result2 = result - (ez * z) local result2 = result - (ez * z)
local rounded1, rounded2 = result1:round(), result2:round() local rounded1, rounded2 = result1:round(0.01), result2:round(0.01)
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
return rounded1, rounded2 return rounded1, rounded2
else else
return rounded1 return rounded1
end end
end end
return result:round() return result:round(0.01)
end end
local function narrow( p1, p2, fix ) local function narrow( p1, p2, fix )
local dist1 = math.abs( (p1 - fix.position):length() - fix.distance ) local dist1 = math.abs( (p1 - fix.position):length() - fix.distance )
local dist2 = math.abs( (p2 - fix.position):length() - fix.distance ) local dist2 = math.abs( (p2 - fix.position):length() - fix.distance )
if math.abs(dist1 - dist2) < 0.05 then if math.abs(dist1 - dist2) < 0.01 then
return p1, p2 return p1, p2
elseif dist1 < dist2 then elseif dist1 < dist2 then
return p1:round() return p1:round(0.01)
else else
return p2:round() return p2:round(0.01)
end end
end end
-- end stock gps api -- end stock gps api
@@ -98,7 +152,7 @@ function GPS.trilaterate(tFixes)
if pos2 then if pos2 then
pos1, pos2 = narrow(pos1, pos2, tFixes[1]) pos1, pos2 = narrow(pos1, pos2, tFixes[1])
end end
if not pos2 then if not pos2 and pos1 and not (pos1.x ~= pos1.x) then
return pos1, attemps return pos1, attemps
end end
end end

View File

@@ -50,18 +50,20 @@ function input:toCode(ch, code)
table.insert(result, 'alt') table.insert(result, 'alt')
end end
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or if ch then -- some weird things happen with control/command on mac
code == keys.leftShift or code == keys.rightShift then if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or
if code and modifiers[code] then code == keys.leftShift or code == keys.rightShift then
table.insert(result, 'shift') if code and modifiers[code] then
elseif #ch == 1 then table.insert(result, 'shift')
table.insert(result, ch:upper()) elseif #ch == 1 then
else table.insert(result, ch:upper())
table.insert(result, 'shift') else
table.insert(result, 'shift')
table.insert(result, ch)
end
elseif not code or not modifiers[code] then
table.insert(result, ch) table.insert(result, ch)
end end
elseif not code or not modifiers[code] then
table.insert(result, ch)
end end
return table.concat(result, '-') return table.concat(result, '-')
@@ -118,6 +120,7 @@ function input:translate(event, code, p1, p2)
local buttons = { 'mouse_click', 'mouse_rightclick' } local buttons = { 'mouse_click', 'mouse_rightclick' }
self.mch = buttons[code] self.mch = buttons[code]
self.mfired = nil self.mfired = nil
self.anchor = { x = p1, y = p2 }
return { return {
code = input:toCode('mouse_down', 255), code = input:toCode('mouse_down', 255),
button = code, button = code,
@@ -132,6 +135,8 @@ function input:translate(event, code, p1, p2)
button = code, button = code,
x = p1, x = p1,
y = p2, y = p2,
dx = p1 - self.anchor.x,
dy = p2 - self.anchor.y,
} }
elseif event == 'mouse_up' then elseif event == 'mouse_up' then
@@ -141,18 +146,26 @@ function input:translate(event, code, p1, p2)
p1 == self.x and p2 == self.y and p1 == self.x and p2 == self.y and
(clock - self.timer < .5) then (clock - self.timer < .5) then
self.mch = 'mouse_doubleclick' self.clickCount = self.clickCount + 1
self.timer = nil if self.clickCount == 3 then
self.mch = 'mouse_tripleclick'
self.timer = nil
self.clickCount = 1
else
self.mch = 'mouse_doubleclick'
end
else else
self.timer = os.clock() self.timer = os.clock()
self.x = p1 self.x = p1
self.y = p2 self.y = p2
self.clickCount = 1
end end
self.mfired = input:toCode(self.mch, 255) self.mfired = input:toCode(self.mch, 255)
else else
self.mch = 'mouse_up' self.mch = 'mouse_up'
self.mfired = input:toCode(self.mch, 255) self.mfired = input:toCode(self.mch, 255)
end end
return { return {
code = self.mfired, code = self.mfired,
button = code, button = code,

View File

@@ -1,16 +1,19 @@
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local NFT = { } local NFT = { }
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/ -- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
local tColourLookup = { } local hexToColor = { }
for n = 1, 16 do for n = 1, 16 do
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1) hexToColor[string.sub("0123456789abcdef", n, n)] = 2 ^ (n - 1)
end end
local colorToHex = Util.transpose(hexToColor)
local function getColourOf(hex) local function getColourOf(hex)
return tColourLookup[hex:byte()] return hexToColor[hex]
end end
function NFT.parse(imageText) function NFT.parse(imageText)
@@ -62,8 +65,22 @@ function NFT.parse(imageText)
return image return image
end end
function NFT.load(path) function NFT.transparency(image)
for y = 1, image.height do
for _,key in pairs(Util.keys(image.fg[y])) do
if image.fg[y][key] == colors.magenta then
image.fg[y][key] = nil
end
end
for _,key in pairs(Util.keys(image.bg[y])) do
if image.bg[y][key] == colors.magenta then
image.bg[y][key] = nil
end
end
end
end
function NFT.load(path)
local imageText = Util.readFile(path) local imageText = Util.readFile(path)
if not imageText then if not imageText then
error('Unable to read image file') error('Unable to read image file')
@@ -71,4 +88,35 @@ function NFT.load(path)
return NFT.parse(imageText) return NFT.parse(imageText)
end end
function NFT.save(image, filename)
local bgcode, txcode = '\30', '\31'
local output = { }
for y = 1, image.height do
local lastBG, lastFG
if image.text[y] then
for x = 1, #image.text[y] do
local bg = image.bg[y][x] or colors.magenta
if bg ~= lastBG then
lastBG = bg
table.insert(output, bgcode .. colorToHex[bg])
end
local fg = image.fg[y][x] or colors.magenta
if fg ~= lastFG then
lastFG = fg
table.insert(output, txcode .. colorToHex[fg])
end
table.insert(output, image.text[y][x])
end
end
if y < image.height then
table.insert(output, '\n')
end
end
Util.writeFile(filename, table.concat(output))
end
return NFT return NFT

View File

@@ -126,7 +126,11 @@ function Socket.connect(host, port, options)
local socket = newSocket(host == os.getComputerID()) local socket = newSocket(host == os.getComputerID())
socket.dhost = tonumber(host) socket.dhost = tonumber(host)
socket.privKey, socket.pubKey = network.getKeyPair() if options and options.keypair then
socket.privKey, socket.pubKey = unpack(options.keypair)
else
socket.privKey, socket.pubKey = network.getKeyPair()
end
local identifier = options and options.identifier or Security.getIdentifier() local identifier = options and options.identifier or Security.getIdentifier()
socket.transmit(port, socket.sport, { socket.transmit(port, socket.sport, {
@@ -135,7 +139,7 @@ function Socket.connect(host, port, options)
dhost = socket.dhost, dhost = socket.dhost,
t = Crypto.encrypt({ -- this is not that much data... t = Crypto.encrypt({ -- this is not that much data...
ts = os.epoch('utc'), ts = os.epoch('utc'),
pk = Util.byteArrayToHex(socket.pubKey), pk = socket.pubKey:toHex(),
}, Util.hexToByteArray(identifier)), }, Util.hexToByteArray(identifier)),
}) })
@@ -233,7 +237,7 @@ function Socket.server(port, options)
type = 'CONN', type = 'CONN',
dhost = socket.dhost, dhost = socket.dhost,
shost = socket.shost, shost = socket.shost,
pk = Util.byteArrayToHex(socket.pubKey), pk = socket.pubKey:toHex(),
options = socket.options.ENCRYPT and { ENCRYPT = true }, options = socket.options.ENCRYPT and { ENCRYPT = true },
}) })

View File

@@ -36,61 +36,66 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
local maxScroll = 100 local maxScroll = 100
local cx, cy = 1, 1 local cx, cy = 1, 1
local blink = false local blink = false
local bg, fg = parent.getBackgroundColor(), parent.getTextColor() local _bg, _fg = parent.getBackgroundColor(), parent.getTextColor()
local canvas = Canvas({ win.canvas = Canvas({
x = sx, x = sx,
y = sy, y = sy,
width = w, width = w,
height = h, height = h,
isColor = parent.isColor(), isColor = parent.isColor(),
offy = 0, offy = 0,
bg = _bg,
fg = _fg,
}) })
win.canvas = canvas
local function update() local function update()
if isVisible then if isVisible then
canvas:render(parent) win.canvas:render(parent)
win.setCursorPos(cx, cy) win.setCursorPos(cx, cy)
end end
end end
local function scrollTo(y) local function scrollTo(y)
y = math.max(0, y) y = math.max(0, y)
y = math.min(#canvas.lines - canvas.height, y) y = math.min(#win.canvas.lines - win.canvas.height, y)
if y ~= canvas.offy then if y ~= win.canvas.offy then
canvas.offy = y win.canvas.offy = y
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
function win.write(str) function win.write(str)
str = tostring(str) or '' str = tostring(str) or ''
canvas:write(cx, cy + canvas.offy, str, bg, fg) win.canvas:write(cx, cy + win.canvas.offy, str, win.canvas.bg, win.canvas.fg)
win.setCursorPos(cx + #str, cy) win.setCursorPos(cx + #str, cy)
update() update()
end end
function win.blit(str, fg, bg) function win.blit(str, fg, bg)
canvas:blit(cx, cy + canvas.offy, str, bg, fg) win.canvas:blit(cx, cy + win.canvas.offy, str, bg, fg)
win.setCursorPos(cx + #str, cy) win.setCursorPos(cx + #str, cy)
update() update()
end end
function win.clear() function win.clear()
canvas.offy = 0 win.canvas.offy = 0
for i = #canvas.lines, canvas.height + 1, -1 do for i = #win.canvas.lines, win.canvas.height + 1, -1 do
canvas.lines[i] = nil win.canvas.lines[i] = nil
end end
canvas:clear(bg, fg) win.canvas:clear()
update() update()
end end
function win.getLine(n)
local line = win.canvas.lines[n]
return line.text, line.fg, line.bg
end
function win.clearLine() function win.clearLine()
canvas:clearLine(cy + canvas.offy, bg, fg) win.canvas:clearLine(cy + win.canvas.offy)
win.setCursorPos(cx, cy) win.setCursorPos(cx, cy)
update() update()
end end
@@ -102,10 +107,14 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.setCursorPos(x, y) function win.setCursorPos(x, y)
cx, cy = math.floor(x), math.floor(y) cx, cy = math.floor(x), math.floor(y)
if isVisible then if isVisible then
parent.setCursorPos(cx + canvas.x - 1, cy + canvas.y - 1) parent.setCursorPos(cx + win.canvas.x - 1, cy + win.canvas.y - 1)
end end
end end
function win.getCursorBlink()
return blink
end
function win.setCursorBlink(b) function win.setCursorBlink(b)
blink = b blink = b
if isVisible then if isVisible then
@@ -114,12 +123,12 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end end
function win.isColor() function win.isColor()
return canvas.isColor return win.canvas.isColor
end end
win.isColour = win.isColor win.isColour = win.isColor
function win.setTextColor(c) function win.setTextColor(c)
fg = c win.canvas.fg = c
end end
win.setTextColour = win.setTextColor win.setTextColour = win.setTextColor
@@ -139,38 +148,38 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
win.setPaletteColour = win.setPaletteColor win.setPaletteColour = win.setPaletteColor
function win.setBackgroundColor(c) function win.setBackgroundColor(c)
bg = c win.canvas.bg = c
end end
win.setBackgroundColour = win.setBackgroundColor win.setBackgroundColour = win.setBackgroundColor
function win.getSize() function win.getSize()
return canvas.width, canvas.height return win.canvas.width, win.canvas.height
end end
function win.scroll(n) function win.scroll(n)
n = n or 1 n = n or 1
if n > 0 then if n > 0 then
local lines = #canvas.lines local lines = #win.canvas.lines
for i = 1, n do for i = 1, n do
canvas.lines[lines + i] = { } win.canvas.lines[lines + i] = { }
canvas:clearLine(lines + i, bg, fg) win.canvas:clearLine(lines + i)
end end
while #canvas.lines > maxScroll do while #win.canvas.lines > maxScroll do
table.remove(canvas.lines, 1) table.remove(win.canvas.lines, 1)
end end
scrollTo(#canvas.lines) scrollTo(#win.canvas.lines)
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
function win.getTextColor() function win.getTextColor()
return fg return win.canvas.fg
end end
win.getTextColour = win.getTextColor win.getTextColour = win.getTextColor
function win.getBackgroundColor() function win.getBackgroundColor()
return bg return win.canvas.bg
end end
win.getBackgroundColour = win.getBackgroundColor win.getBackgroundColour = win.getBackgroundColor
@@ -178,7 +187,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
if visible ~= isVisible then if visible ~= isVisible then
isVisible = visible isVisible = visible
if isVisible then if isVisible then
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
@@ -186,7 +195,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.redraw() function win.redraw()
if isVisible then if isVisible then
canvas:dirty() win.canvas:dirty()
update() update()
end end
end end
@@ -194,27 +203,27 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
function win.restoreCursor() function win.restoreCursor()
if isVisible then if isVisible then
win.setCursorPos(cx, cy) win.setCursorPos(cx, cy)
win.setTextColor(fg) win.setTextColor(win.canvas.fg)
win.setCursorBlink(blink) win.setCursorBlink(blink)
end end
end end
function win.getPosition() function win.getPosition()
return canvas.x, canvas.y return win.canvas.x, win.canvas.y
end end
function win.reposition(x, y, width, height) function win.reposition(x, y, width, height)
canvas.x, canvas.y = x, y win.canvas.x, win.canvas.y = x, y
canvas:resize(width or canvas.width, height or canvas.height) win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
end end
--[[ Additional methods ]]-- --[[ Additional methods ]]--
function win.scrollDown() function win.scrollDown()
scrollTo(canvas.offy + 1) scrollTo(win.canvas.offy + 1)
end end
function win.scrollUp() function win.scrollUp()
scrollTo(canvas.offy - 1) scrollTo(win.canvas.offy - 1)
end end
function win.scrollTop() function win.scrollTop()
@@ -222,7 +231,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end end
function win.scrollBottom() function win.scrollBottom()
scrollTo(#canvas.lines) scrollTo(#win.canvas.lines)
end end
function win.setMaxScroll(ms) function win.setMaxScroll(ms)
@@ -230,37 +239,35 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end end
function win.getCanvas() function win.getCanvas()
return canvas return win.canvas
end end
function win.getParent() function win.getParent()
return parent return parent
end end
canvas:clear() win.canvas:clear()
return win return win
end end
-- get windows contents -- get windows contents
function Terminal.getContents(win, parent) function Terminal.getContents(win)
local oblit, oscp = parent.blit, parent.setCursorPos if not win.getLine then
local lines = { } error('window is required')
end
parent.blit = function(text, fg, bg) local lines = { }
lines[#lines + 1] = { local _, h = win.getSize()
for i = 1, h do
local text, fg, bg = win.getLine(i)
lines[i] = {
text = text, text = text,
fg = fg, fg = fg,
bg = bg, bg = bg,
} }
end end
parent.setCursorPos = function() end
win.setVisible(true)
win.redraw()
parent.blit = oblit
parent.setCursorPos = oscp
return lines return lines
end end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
local colors = _G.colors
local _rep = string.rep
local _sub = string.sub
local Blit = { }
Blit.colorPalette = { }
Blit.grayscalePalette = { }
for n = 1, 16 do
Blit.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n)
Blit.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
end
-- default palette
Blit.palette = Blit.colorPalette
function Blit:init(t, args)
if args then
for k,v in pairs(args) do
self[k] = v
end
end
if type(t) == 'string' then
-- create a blit from a string
self.text, self.bg, self.fg = Blit.toblit(t, args or { })
elseif type(t) == 'number' then
-- create a fixed width blit
self.width = t
self.text = _rep(' ', self.width)
self.bg = _rep(self.palette[args.bg], self.width)
self.fg = _rep(self.palette[args.fg], self.width)
else
self.text = t.text
self.bg = t.bg
self.fg = t.fg
end
end
function Blit:write(x, text, bg, fg)
self:insert(x, text,
bg and _rep(self.palette[bg], #text),
fg and _rep(self.palette[fg], #text))
end
function Blit:insert(x, text, bg, fg)
if x <= self.width then
local width = #text
local tx, tex
if x < 1 then
tx = 2 - x
width = width + x - 1
x = 1
end
if x + width - 1 > self.width then
tex = self.width - x + (tx or 1)
width = tex - (tx or 1) + 1
end
if width > 0 then
local function replace(sstr, rstr)
if tx or tex then
rstr = _sub(rstr, tx or 1, tex)
end
if x == 1 and width == self.width then
return rstr
elseif x == 1 then
return rstr .. _sub(sstr, x + width)
elseif x + width > self.width then
return _sub(sstr, 1, x - 1) .. rstr
end
return _sub(sstr, 1, x - 1) .. rstr .. _sub(sstr, x + width)
end
self.text = replace(self.text, text)
if fg then
self.fg = replace(self.fg, fg)
end
if bg then
self.bg = replace(self.bg, bg)
end
end
end
end
function Blit:sub(s, e)
return Blit({
text = self.text:sub(s, e),
bg = self.bg:sub(s, e),
fg = self.fg:sub(s, e),
})
end
function Blit:wrap(max)
local lines = { }
local data = self
repeat
if #data.text <= max then
table.insert(lines, data)
break
elseif data.text:sub(max+1, max+1) == ' ' then
table.insert(lines, data:sub(1, max))
data = data:sub(max + 2)
else
local x = data.text:sub(1, max)
local s = x:match('(.*) ') or x
table.insert(lines, data:sub(1, #s))
data = data:sub(#s + 1)
end
local t = data.text:match('^%s*(.*)')
local spaces = #data.text - #t
if spaces > 0 then
data = data:sub(spaces + 1)
end
until not data.text or #data.text == 0
return lines
end
-- convert a string of text to blit format doing color conversion
-- and processing ansi color sequences
function Blit.toblit(str, cs)
local text, fg, bg = '', '', ''
if not cs.cbg then
-- reset colors
cs.rbg = cs.bg or colors.black
cs.rfg = cs.fg or colors.white
-- current colors
cs.cbg = cs.rbg
cs.cfg = cs.rfg
cs.palette = cs.palette or Blit.palette
end
str = str:gsub('(.-)\027%[([%d;]+)m',
function(k, seq)
text = text .. k
bg = bg .. string.rep(cs.palette[cs.cbg], #k)
fg = fg .. string.rep(cs.palette[cs.cfg], #k)
for color in string.gmatch(seq, "%d+") do
color = tonumber(color)
if color == 0 then
-- reset to default
cs.cfg = cs.rfg
cs.cbg = cs.rbg
elseif color > 20 then
cs.cbg = 2 ^ (color - 21)
else
cs.cfg = 2 ^ (color - 1)
end
end
return k
end)
local k = str:sub(#text + 1)
return text .. k,
bg .. string.rep(cs.palette[cs.cbg], #k),
fg .. string.rep(cs.palette[cs.cfg], #k)
end
return setmetatable(Blit, {
__call = function(_, ...)
local obj = setmetatable({ }, { __index = Blit })
obj:init(...)
return obj
end
})

View File

@@ -9,27 +9,34 @@ local colors = _G.colors
local Canvas = class() local Canvas = class()
Canvas.colorPalette = { } local function genPalette(map)
Canvas.darkPalette = { } local t = { }
Canvas.grayscalePalette = { } local rcolors = Util.transpose(colors)
for n = 1, 16 do
for n = 1, 16 do local pow = 2 ^ (n - 1)
Canvas.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) local ch = _sub(map, n, n)
Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n) t[pow] = ch
Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n) t[rcolors[pow]] = ch
end
return t
end end
Canvas.colorPalette = genPalette('0123456789abcdef')
Canvas.grayscalePalette = genPalette('088888878877787f')
--[[ --[[
A canvas can have more lines than canvas.height in order to scroll A canvas can have more lines than canvas.height in order to scroll
]]
TODO: finish vertical scrolling
]]
function Canvas:init(args) function Canvas:init(args)
self.x = 1 self.bg = colors.black
self.y = 1 self.fg = colors.white
self.layers = { }
Util.merge(self, args) Util.merge(self, args)
self.x = self.x or 1
self.y = self.y or 1
self.ex = self.x + self.width - 1 self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1 self.ey = self.y + self.height - 1
@@ -45,16 +52,31 @@ function Canvas:init(args)
for i = 1, self.height do for i = 1, self.height do
self.lines[i] = { } self.lines[i] = { }
end end
self:clear()
end end
function Canvas:move(x, y) function Canvas:move(x, y)
self.x, self.y = x, y self.x, self.y = x, y
self.ex = self.x + self.width - 1 self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1 self.ey = self.y + self.height - 1
if self.parent then
self.parent:dirty(true)
end
end end
function Canvas:resize(w, h) function Canvas:resize(w, h)
for i = #self.lines, h do self:resizeBuffer(w, h)
self.ex = self.x + w - 1
self.ey = self.y + h - 1
self.width = w
self.height = h
end
-- resize the canvas buffer - not the canvas itself
function Canvas:resizeBuffer(w, h)
for i = #self.lines + 1, h do
self.lines[i] = { } self.lines[i] = { }
self:clearLine(i) self:clearLine(i)
end end
@@ -65,26 +87,24 @@ function Canvas:resize(w, h)
if w < self.width then if w < self.width then
for i = 1, h do for i = 1, h do
self.lines[i].text = _sub(self.lines[i].text, 1, w) local ln = self.lines[i]
self.lines[i].fg = _sub(self.lines[i].fg, 1, w) ln.text = _sub(ln.text, 1, w)
self.lines[i].bg = _sub(self.lines[i].bg, 1, w) ln.fg = _sub(ln.fg, 1, w)
ln.bg = _sub(ln.bg, 1, w)
end end
elseif w > self.width then elseif w > self.width then
local d = w - self.width local d = w - self.width
local text = _rep(' ', d) local text = _rep(' ', d)
local fg = _rep(self.palette[self.fg or colors.white], d) local fg = _rep(self.palette[self.fg], d)
local bg = _rep(self.palette[self.bg or colors.black], d) local bg = _rep(self.palette[self.bg], d)
for i = 1, h do for i = 1, h do
self.lines[i].text = self.lines[i].text .. text local ln = self.lines[i]
self.lines[i].fg = self.lines[i].fg .. fg ln.text = ln.text .. text
self.lines[i].bg = self.lines[i].bg .. bg ln.fg = ln.fg .. fg
ln.bg = ln.bg .. bg
ln.dirty = true
end end
end end
self.ex = self.x + w - 1
self.ey = self.y + h - 1
self.width = w
self.height = h
end end
function Canvas:copy() function Canvas:copy()
@@ -104,30 +124,26 @@ function Canvas:copy()
end end
function Canvas:addLayer(layer) function Canvas:addLayer(layer)
local canvas = Canvas({ layer.parent = self
x = layer.x, if not self.children then
y = layer.y, self.children = { }
width = layer.width, end
height = layer.height, table.insert(self.children, 1, layer)
isColor = self.isColor, return layer
})
canvas.parent = self
table.insert(self.layers, canvas)
return canvas
end end
function Canvas:removeLayer() function Canvas:removeLayer()
for k, layer in pairs(self.parent.layers) do for k, layer in pairs(self.parent.children) do
if layer == self then if layer == self then
self:setVisible(false) self:setVisible(false)
table.remove(self.parent.layers, k) table.remove(self.parent.children, k)
break break
end end
end end
end end
function Canvas:setVisible(visible) function Canvas:setVisible(visible)
self.visible = visible self.visible = visible -- TODO: use self.active = visible
if not visible and self.parent then if not visible and self.parent then
self.parent:dirty() self.parent:dirty()
-- TODO: set parent's lines to dirty for each line in self -- TODO: set parent's lines to dirty for each line in self
@@ -136,11 +152,10 @@ end
-- Push a layer to the top -- Push a layer to the top
function Canvas:raise() function Canvas:raise()
if self.parent then if self.parent and self.parent.children then
local layers = self.parent.layers or { } for k, v in pairs(self.parent.children) do
for k, v in pairs(layers) do
if v == self then if v == self then
table.insert(layers, table.remove(layers, k)) table.insert(self.parent.children, table.remove(self.parent.children, k))
break break
end end
end end
@@ -160,54 +175,42 @@ end
function Canvas:blit(x, y, text, bg, fg) function Canvas:blit(x, y, text, bg, fg)
if y > 0 and y <= #self.lines and x <= self.width then if y > 0 and y <= #self.lines and x <= self.width then
local width = #text local width = #text
local tx, tex
-- fix ffs
if x < 1 then if x < 1 then
text = _sub(text, 2 - x) tx = 2 - x
if bg then
bg = _sub(bg, 2 - x)
end
if fg then
fg = _sub(fg, 2 - x)
end
width = width + x - 1 width = width + x - 1
x = 1 x = 1
end end
if x + width - 1 > self.width then if x + width - 1 > self.width then
text = _sub(text, 1, self.width - x + 1) tex = self.width - x + (tx or 1)
if bg then width = tex - (tx or 1) + 1
bg = _sub(bg, 1, self.width - x + 1)
end
if fg then
fg = _sub(fg, 1, self.width - x + 1)
end
width = #text
end end
if width > 0 then if width > 0 then
local function replace(sstr, rstr)
local function replace(sstr, pos, rstr) if tx or tex then
if pos == 1 and width == self.width then rstr = _sub(rstr, tx or 1, tex)
return rstr
elseif pos == 1 then
return rstr .. _sub(sstr, pos+width)
elseif pos + width > self.width then
return _sub(sstr, 1, pos-1) .. rstr
end end
return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) if x == 1 and width == self.width then
return rstr
elseif x == 1 then
return rstr .. _sub(sstr, x + width)
elseif x + width > self.width then
return _sub(sstr, 1, x - 1) .. rstr
end
return _sub(sstr, 1, x - 1) .. rstr .. _sub(sstr, x + width)
end end
local line = self.lines[y] local line = self.lines[y]
if line then line.dirty = true
line.dirty = true line.text = replace(line.text, text)
line.text = replace(line.text, x, text, width) if fg then
if fg then line.fg = replace(line.fg, fg)
line.fg = replace(line.fg, x, fg, width) end
end if bg then
if bg then line.bg = replace(line.bg, bg)
line.bg = replace(line.bg, x, bg, width)
end
end end
end end
end end
@@ -223,15 +226,15 @@ function Canvas:writeLine(y, text, fg, bg)
end end
function Canvas:clearLine(y, bg, fg) function Canvas:clearLine(y, bg, fg)
fg = _rep(self.palette[fg or colors.white], self.width) fg = _rep(self.palette[fg or self.fg], self.width)
bg = _rep(self.palette[bg or colors.black], self.width) bg = _rep(self.palette[bg or self.bg], self.width)
self:writeLine(y, _rep(' ', self.width), fg, bg) self:writeLine(y, _rep(' ', self.width), fg, bg)
end end
function Canvas:clear(bg, fg) function Canvas:clear(bg, fg)
local text = _rep(' ', self.width) local text = _rep(' ', self.width)
fg = _rep(self.palette[fg or colors.white], self.width) fg = _rep(self.palette[fg or self.fg], self.width)
bg = _rep(self.palette[bg or colors.black], self.width) bg = _rep(self.palette[bg or self.bg], self.width)
for i = 1, #self.lines do for i = 1, #self.lines do
self:writeLine(i, text, fg, bg) self:writeLine(i, text, fg, bg)
end end
@@ -245,13 +248,16 @@ function Canvas:isDirty()
end end
end end
function Canvas:dirty() function Canvas:dirty(includingChildren)
for i = 1, #self.lines do if self.lines then
self.lines[i].dirty = true for i = 1, #self.lines do
end self.lines[i].dirty = true
if self.layers then end
for _, canvas in pairs(self.layers) do
canvas:dirty() if includingChildren and self.children then
for _, child in pairs(self.children) do
child:dirty(true)
end
end end
end end
end end
@@ -277,123 +283,110 @@ function Canvas:applyPalette(palette)
self.palette = palette self.palette = palette
end end
function Canvas:render(device) -- either render directly to the device
local offset = { x = 0, y = 0 } -- or use another canvas as a backing buffer
local parent = self.parent function Canvas:render(device, doubleBuffer)
while parent do self.regions = Region.new(self.x, self.y, self.ex, self.ey)
offset.x = offset.x + parent.x - 1 self:__renderLayers(device, { x = self.x - 1, y = self.y - 1 }, doubleBuffer)
offset.y = offset.y + parent.y - 1
parent = parent.parent -- doubleBuffering to reduce the amount of
end -- setCursorPos, blits
if #self.layers > 0 then if doubleBuffer then
self:__renderLayers(device, offset) --[[
else local drew = false
self:__blitRect(device, nil, { local bg = _rep(2, device.width)
x = self.x + offset.x, for k,v in pairs(device.lines) do
y = self.y + offset.y if v.dirty then
}) device.device.setCursorPos(device.x, device.y + k - 1)
self:clean() device.device.blit(v.text, v.fg, bg)
drew = true
end
end
if drew then
local c = os.clock()
repeat until os.clock()-c > .1
end
]]
for k,v in pairs(device.lines) do
if v.dirty then
device.device.setCursorPos(device.x, device.y + k - 1)
device.device.blit(v.text, v.fg, v.bg)
v.dirty = false
end
end
end end
end end
-- regions are comprised of absolute values that coorespond to the output device. -- regions are comprised of absolute values that correspond to the output device.
-- canvases have coordinates relative to their parent. -- canvases have coordinates relative to their parent.
-- canvas layer's stacking order is determined by the position within the array. -- canvas layer's stacking order is determined by the position within the array.
-- layers in the beginning of the array are overlayed by layers further down in -- layers in the beginning of the array are overlayed by layers further down in
-- the array. -- the array.
function Canvas:__renderLayers(device, offset) function Canvas:__renderLayers(device, offset, doubleBuffer)
if #self.layers > 0 then if self.children then
self.regions = self.regions or Region.new(self.x, self.y, self.ex, self.ey) for i = #self.children, 1, -1 do
local canvas = self.children[i]
for i = 1, #self.layers do if canvas.visible or canvas.enabled then
local canvas = self.layers[i]
if canvas.visible then
-- punch out this area from the parent's canvas
self:__punch(canvas, offset)
-- get the area to render for this layer -- get the area to render for this layer
canvas.regions = Region.new( canvas.regions = Region.new(
canvas.x + offset.x, canvas.x + offset.x - (self.offx or 0),
canvas.y + offset.y, canvas.y + offset.y - (self.offy or 0),
canvas.ex + offset.x, canvas.ex + offset.x - (self.offx or 0),
canvas.ey + offset.y) canvas.ey + offset.y - (self.offy or 0))
-- contain within parent
canvas.regions:andRegion(self.regions)
-- punch out this area from the parent's canvas
self.regions:subRect(
canvas.x + offset.x - (self.offx or 0),
canvas.y + offset.y - (self.offy or 0),
canvas.ex + offset.x - (self.offx or 0),
canvas.ey + offset.y - (self.offy or 0))
-- punch out any layers that overlap this one
for j = i + 1, #self.layers do
if self.layers[j].visible then
canvas:__punch(self.layers[j], offset)
end
end
if #canvas.regions.region > 0 then if #canvas.regions.region > 0 then
canvas:__renderLayers(device, { canvas:__renderLayers(device, {
x = canvas.x + offset.x - 1, x = canvas.x + offset.x - 1 - (self.offx or 0),
y = canvas.y + offset.y - 1, y = canvas.y + offset.y - 1 - (self.offy or 0),
}) }, doubleBuffer)
end end
canvas.regions = nil canvas.regions = nil
end end
end end
self:__blitClipped(device, offset)
self.regions = nil
elseif self.regions and #self.regions.region > 0 then
self:__blitClipped(device, offset)
self.regions = nil
else
self:__blitRect(device, nil, {
x = self.x + offset.x,
y = self.y + offset.y
})
self.regions = nil
end end
self:clean()
end
function Canvas:__blitClipped(device, offset)
for _,region in ipairs(self.regions.region) do for _,region in ipairs(self.regions.region) do
self:__blitRect(device, self:__blitRect(device,
{ x = region[1] - offset.x, { x = region[1] - offset.x,
y = region[2] - offset.y, y = region[2] - offset.y,
ex = region[3] - offset.x, ex = region[3] - offset.x,
ey = region[4] - offset.y}, ey = region[4] - offset.y },
{ x = region[1], y = region[2] }) { x = region[1], y = region[2] }, doubleBuffer)
end end
self.regions = nil
self:clean()
end end
function Canvas:__punch(rect, offset) function Canvas:__blitRect(device, src, tgt, doubleBuffer)
self.regions:subRect( -- for visualizing updates on the screen
rect.x + offset.x,
rect.y + offset.y,
rect.ex + offset.x,
rect.ey + offset.y)
end
function Canvas:__blitRect(device, src, tgt)
src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 }
tgt = tgt or self
--[[ --[[
-- for visualizing updates on the screen if Canvas.__visualize or self.visualize then
local drew local drew
for i = 0, src.ey - src.y do local t = _rep(' ', src.ex-src.x + 1)
local line = self.lines[src.y + i + (self.offy or 0)] local bg = _rep(2, src.ex-src.x + 1)
if line and line.dirty then for i = 0, src.ey - src.y do
drew = true local line = self.lines[src.y + i + (self.offy or 0)]
local t, fg, bg = line.text, line.fg, line.bg if line and line.dirty then
if src.x > 1 or src.ex < self.ex then drew = true
t = _sub(t, src.x, src.ex) device.setCursorPos(tgt.x, tgt.y + i)
fg = _rep(1, src.ex-src.x + 1) device.blit(t, bg, bg)
bg = _rep(2, src.ex-src.x + 1)
end end
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, fg, bg)
end end
end if drew then
if drew then local c = os.clock()
os.sleep(.3) repeat until os.clock()-c > .03
end
end end
]] ]]
for i = 0, src.ey - src.y do for i = 0, src.ey - src.y do
@@ -405,8 +398,13 @@ function Canvas:__blitRect(device, src, tgt)
fg = _sub(fg, src.x, src.ex) fg = _sub(fg, src.x, src.ex)
bg = _sub(bg, src.x, src.ex) bg = _sub(bg, src.x, src.ex)
end end
device.setCursorPos(tgt.x, tgt.y + i) if doubleBuffer then
device.blit(t, fg, bg) Canvas.blit(device, tgt.x, tgt.y + i,
t, bg, fg)
else
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, fg, bg)
end
end end
end end
end end

View File

@@ -1,32 +0,0 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.ActiveLayer = class(UI.Window)
UI.ActiveLayer.defaults = {
UIElement = 'ActiveLayer',
}
function UI.ActiveLayer:layout()
UI.Window.layout(self)
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.ActiveLayer:enable(...)
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...)
if self.parent.transitionHint then
self:addTransition(self.parent.transitionHint)
end
self:focusFirst()
end
function UI.ActiveLayer:disable()
if self.canvas then
self.canvas:setVisible(false)
end
UI.Window.disable(self)
end

View File

@@ -2,32 +2,30 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.Button = class(UI.Window) UI.Button = class(UI.Window)
UI.Button.defaults = { UI.Button.defaults = {
UIElement = 'Button', UIElement = 'Button',
text = 'button', text = 'button',
backgroundColor = colors.lightGray, backgroundColor = 'lightGray',
backgroundFocusColor = colors.gray, backgroundFocusColor = 'gray',
textFocusColor = colors.white, textFocusColor = 'white',
textInactiveColor = colors.gray, textInactiveColor = 'gray',
textColor = colors.black, textColor = 'black',
centered = true, centered = true,
height = 1, height = 1,
focusIndicator = ' ', focusIndicator = ' ',
event = 'button_press', event = 'button_press',
accelerators = { accelerators = {
space = 'button_activate', [ ' ' ] = 'button_activate',
enter = 'button_activate', enter = 'button_activate',
mouse_click = 'button_activate', mouse_click = 'button_activate',
} }
} }
function UI.Button:setParent() function UI.Button:layout()
if not self.width and not self.ex then if not self.width and not self.ex then
self.width = #self.text + 2 self.width = self.noPadding and #self.text or #self.text + 2
end end
UI.Window.setParent(self) UI.Window.layout(self)
end end
function UI.Button:draw() function UI.Button:draw()
@@ -35,13 +33,13 @@ function UI.Button:draw()
local bg = self.backgroundColor local bg = self.backgroundColor
local ind = ' ' local ind = ' '
if self.focused then if self.focused then
bg = self.backgroundFocusColor bg = self:getProperty('backgroundFocusColor')
fg = self.textFocusColor fg = self:getProperty('textFocusColor')
ind = self.focusIndicator ind = self.focusIndicator
elseif self.inactive then elseif self.inactive then
fg = self.textInactiveColor fg = self:getProperty('textInactiveColor')
end end
local text = ind .. self.text .. ' ' local text = self.noPadding and self.text or ind .. self.text .. ' '
if self.centered then if self.centered then
self:clear(bg) self:clear(bg)
self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg) self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg)
@@ -59,8 +57,28 @@ end
function UI.Button:eventHandler(event) function UI.Button:eventHandler(event)
if event.type == 'button_activate' then if event.type == 'button_activate' then
self:emit({ type = self.event, button = self }) self:emit({ type = self.event, button = self, element = self })
return true return true
end end
return false return false
end end
function UI.Button.example()
return UI.Window {
button1 = UI.Button {
x = 2, y = 2,
text = 'Press',
},
button2 = UI.Button {
x = 2, y = 4,
backgroundColor = 'green',
event = 'custom_event',
},
button3 = UI.Button {
x = 12, y = 2,
height = 5,
event = 'big_event',
text = 'large button'
}
}
end

View File

@@ -1,8 +1,6 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Checkbox = class(UI.Window) UI.Checkbox = class(UI.Window)
UI.Checkbox.defaults = { UI.Checkbox.defaults = {
UIElement = 'Checkbox', UIElement = 'Checkbox',
@@ -11,9 +9,9 @@ UI.Checkbox.defaults = {
leftMarker = UI.extChars and '\124' or '[', leftMarker = UI.extChars and '\124' or '[',
rightMarker = UI.extChars and '\124' or ']', rightMarker = UI.extChars and '\124' or ']',
value = false, value = false,
textColor = colors.white, textColor = 'white',
backgroundColor = colors.black, backgroundColor = 'black',
backgroundFocusColor = colors.lightGray, backgroundFocusColor = 'lightGray',
height = 1, height = 1,
width = 3, width = 3,
accelerators = { accelerators = {
@@ -21,21 +19,18 @@ UI.Checkbox.defaults = {
mouse_click = 'checkbox_toggle', mouse_click = 'checkbox_toggle',
} }
} }
function UI.Checkbox:layout()
self.width = self.label and #self.label + 4 or 3
UI.Window.layout(self)
end
function UI.Checkbox:draw() function UI.Checkbox:draw()
local bg = self.backgroundColor local bg = self.focused and self.backgroundFocusColor or self.backgroundColor
if self.focused then
bg = self.backgroundFocusColor
end
if type(self.value) == 'string' then
self.value = nil -- TODO: fix form
end
local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator)
local x = 1 local x = 1
if self.label then if self.label then
self:write(1, 1, self.label) self:write(1, 1, self.label, self.labelBackgroundColor)
x = #self.label + 2 x = #self.label + 2
end end
self:write(x, 1, text, bg)
self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor) self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor)
self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg) self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg)
self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor) self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor)
@@ -46,11 +41,12 @@ function UI.Checkbox:focus()
end end
function UI.Checkbox:setValue(v) function UI.Checkbox:setValue(v)
self.value = v self.value = not not v
end end
function UI.Checkbox:reset() function UI.Checkbox:reset()
self.value = false self.value = false
self:draw()
end end
function UI.Checkbox:eventHandler(event) function UI.Checkbox:eventHandler(event)
@@ -61,3 +57,15 @@ function UI.Checkbox:eventHandler(event)
return true return true
end end
end end
function UI.Checkbox.example()
return UI.Window {
ex1 = UI.Checkbox {
label = 'test',
x = 2, y = 2,
},
ex2 = UI.Checkbox {
x = 2, y = 4,
},
}
end

View File

@@ -0,0 +1,54 @@
local class = require('opus.class')
local UI = require('opus.ui')
local function safeValue(v)
local t = type(v)
if t == 'string' or t == 'number' then
return v
end
return tostring(v)
end
UI.CheckboxGrid = class(UI.Grid)
UI.CheckboxGrid.defaults = {
UIElement = 'CheckboxGrid',
checkedKey = 'checked',
accelerators = {
space = 'grid_toggle',
},
}
function UI.CheckboxGrid:drawRow(sb, row, focused, bg, fg)
local ind = focused and self.focusIndicator or ' '
for _,col in pairs(self.columns) do
sb:write(ind .. safeValue(row[col.key] or ''),
col.cw + 1,
col.align,
col.backgroundColor or bg,
col.textColor or fg)
ind = ' '
end
end
function UI.CheckboxGrid:eventHandler(event)
if event.type == 'key_enter' and self.selected then
self.selected.checked = not self.selected.checked
self:draw()
self:emit({ type = 'grid_check', checked = self.selected, element = self })
else
return UI.Grid.eventHandler(self, event)
end
end
function UI.CheckboxGrid.example()
return UI.CheckboxGrid {
values = {
{ checked = false, name = 'unchecked' },
{ checked = true, name = 'checked' },
},
columns = {
{ heading = 'Checked', key = 'checked' },
{ heading = 'Data', key = 'name', }
},
}
end

View File

@@ -11,11 +11,16 @@ UI.Chooser.defaults = {
nochoice = 'Select', nochoice = 'Select',
backgroundFocusColor = colors.lightGray, backgroundFocusColor = colors.lightGray,
textInactiveColor = colors.gray, textInactiveColor = colors.gray,
leftIndicator = UI.extChars and '\17' or '<', leftIndicator = UI.extChars and '\171' or '<',
rightIndicator = UI.extChars and '\16' or '>', rightIndicator = UI.extChars and '\187' or '>',
height = 1, height = 1,
accelerators = {
space = 'choice_next',
right = 'choice_next',
left = 'choice_prev',
}
} }
function UI.Chooser:setParent() function UI.Chooser:layout()
if not self.width and not self.ex then if not self.width and not self.ex then
self.width = 1 self.width = 1
for _,v in pairs(self.choices) do for _,v in pairs(self.choices) do
@@ -25,20 +30,15 @@ function UI.Chooser:setParent()
end end
self.width = self.width + 4 self.width = self.width + 4
end end
UI.Window.setParent(self) UI.Window.layout(self)
end end
function UI.Chooser:draw() function UI.Chooser:draw()
local bg = self.backgroundColor local bg = self.focused and self.backgroundFocusColor or self.backgroundColor
if self.focused then
bg = self.backgroundFocusColor
end
local fg = self.inactive and self.textInactiveColor or self.textColor local fg = self.inactive and self.textInactiveColor or self.textColor
local choice = Util.find(self.choices, 'value', self.value) local choice = Util.find(self.choices, 'value', self.value)
local value = self.nochoice local value = choice and choice.name or self.nochoice
if choice then
value = choice.name
end
self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black) self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black)
self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg) self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg)
self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black) self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black)
@@ -49,40 +49,60 @@ function UI.Chooser:focus()
end end
function UI.Chooser:eventHandler(event) function UI.Chooser:eventHandler(event)
if event.type == 'key' then if event.type == 'choice_next' then
if event.key == 'right' or event.key == 'space' then local _,k = Util.find(self.choices, 'value', self.value)
local _,k = Util.find(self.choices, 'value', self.value) local choice
local choice if not k then k = 0 end
if not k then k = 1 end if k and k < #self.choices then
if k and k < #self.choices then choice = self.choices[k+1]
choice = self.choices[k+1] else
else choice = self.choices[1]
choice = self.choices[1]
end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
elseif event.key == 'left' then
local _,k = Util.find(self.choices, 'value', self.value)
local choice
if k and k > 1 then
choice = self.choices[k-1]
else
choice = self.choices[#self.choices]
end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
end end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
elseif event.type == 'choice_prev' then
local _,k = Util.find(self.choices, 'value', self.value)
local choice
if k and k > 1 then
choice = self.choices[k-1]
else
choice = self.choices[#self.choices]
end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
if event.x == 1 then if event.x == 1 then
self:emit({ type = 'key', key = 'left' }) self:emit({ type = 'choice_prev' })
return true return true
elseif event.x == self.width then elseif event.x == self.width then
self:emit({ type = 'key', key = 'right' }) self:emit({ type = 'choice_next' })
return true return true
end end
end end
end end
function UI.Chooser.example()
return UI.Window {
a = UI.Chooser {
x = 2, y = 2,
choices = {
{ name = 'choice1', value = 'value1' },
{ name = 'choice2', value = 'value2' },
{ name = 'choice3', value = 'value3' },
},
value = 'value2',
},
b = UI.Chooser {
x = 2, y = 4,
choices = {
{ name = 'choice1', value = 'value1' },
{ name = 'choice2', value = 'value2' },
{ name = 'choice3', value = 'value3' },
},
}
}
end

View File

@@ -1,15 +1,11 @@
local Canvas = require('opus.ui.canvas')
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Dialog = class(UI.SlideOut) UI.Dialog = class(UI.SlideOut)
UI.Dialog.defaults = { UI.Dialog.defaults = {
UIElement = 'Dialog', UIElement = 'Dialog',
height = 7, height = 7,
textColor = colors.black, noFill = true,
backgroundColor = colors.white,
okEvent ='dialog_ok', okEvent ='dialog_ok',
cancelEvent = 'dialog_cancel', cancelEvent = 'dialog_cancel',
} }
@@ -18,22 +14,36 @@ function UI.Dialog:postInit()
self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title }) self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title })
end end
function UI.Dialog:show(...)
local canvas = self.parent:getCanvas()
self.oldPalette = canvas.palette
canvas:applyPalette(Canvas.darkPalette)
UI.SlideOut.show(self, ...)
end
function UI.Dialog:hide(...)
self.parent:getCanvas().palette = self.oldPalette
UI.SlideOut.hide(self, ...)
self.parent:draw()
end
function UI.Dialog:eventHandler(event) function UI.Dialog:eventHandler(event)
if event.type == 'dialog_cancel' then if event.type == 'dialog_cancel' then
self:hide() self:hide()
end end
return UI.SlideOut.eventHandler(self, event) return UI.SlideOut.eventHandler(self, event)
end end
function UI.Dialog.example()
return UI.Dialog {
title = 'Enter Starting Level',
height = 7,
form = UI.Form {
y = 3, x = 2, height = 4,
event = 'setStartLevel',
cancelEvent = 'slide_hide',
text = UI.Text {
x = 5, y = 1, width = 20,
textColor = 'gray',
},
textEntry = UI.TextEntry {
formKey = 'level',
x = 15, y = 1, width = 7,
},
},
statusBar = UI.StatusBar(),
enable = function(self)
require('opus.event').onTimeout(0, function()
self:show()
self:sync()
end)
end,
}
end

View File

@@ -2,12 +2,10 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.DropMenu = class(UI.MenuBar) UI.DropMenu = class(UI.MenuBar)
UI.DropMenu.defaults = { UI.DropMenu.defaults = {
UIElement = 'DropMenu', UIElement = 'DropMenu',
backgroundColor = colors.white, backgroundColor = 'white',
buttonClass = 'DropMenuItem', buttonClass = 'DropMenuItem',
} }
function UI.DropMenu:layout() function UI.DropMenu:layout()
@@ -32,44 +30,77 @@ function UI.DropMenu:layout()
self.height = #self.children + 1 self.height = #self.children + 1
self.width = maxWidth + 2 self.width = maxWidth + 2
if not self.canvas then if self.x + self.width > self.parent.width then
self.canvas = self:addLayer() self.x = self.parent.width - self.width + 1
else
self.canvas:resize(self.width, self.height)
end end
self:reposition(self.x, self.y, self.width, self.height)
end end
function UI.DropMenu:enable() function UI.DropMenu:enable()
end local menuBar = self.parent:find(self.menuUid)
local hasActive
function UI.DropMenu:show(x, y) for _,c in pairs(self.children) do
self.x, self.y = x, y if not c.spacer and menuBar then
self.canvas:move(x, y) c.inactive = not menuBar:getActive(c)
self.canvas:setVisible(true) end
if not c.inactive then
hasActive = true
end
end
-- jump through a lot of hoops if all selections are inactive
-- there's gotta be a better way
-- lots of exception code just to handle drop menus
self.focus = not hasActive and function() end
UI.Window.enable(self) UI.Window.enable(self)
if self.focus then
self:setFocus(self)
else
self:focusFirst()
end
self:draw() self:draw()
self:capture(self)
self:focusFirst()
end end
function UI.DropMenu:hide() function UI.DropMenu:disable()
self:disable() UI.Window.disable(self)
self.canvas:setVisible(false) self:remove()
self:release(self)
end end
function UI.DropMenu:eventHandler(event) function UI.DropMenu:eventHandler(event)
if event.type == 'focus_lost' and self.enabled then if event.type == 'focus_lost' and self.enabled then
if not Util.contains(self.children, event.focused) then if not (Util.contains(self.children, event.focused) or event.focused == self) then
self:hide() self:disable()
end end
elseif event.type == 'mouse_out' and self.enabled then elseif event.type == 'mouse_out' and self.enabled then
self:hide() self:disable()
self:refocus() self:setFocus(self.parent:find(self.lastFocus))
else else
return UI.MenuBar.eventHandler(self, event) return UI.MenuBar.eventHandler(self, event)
end end
return true return true
end end
function UI.DropMenu.example()
return UI.MenuBar {
buttons = {
{ text = 'File', dropdown = {
{ text = 'Run', event = 'run' },
{ text = 'Shell s', event = 'shell' },
{ spacer = true },
{ text = 'Quit ^q', event = 'quit' },
} },
{ text = 'Edit', dropdown = {
{ text = 'Copy', event = 'run' },
{ text = 'Paste s', event = 'shell' },
} },
{ text = '\187',
x = -3,
dropdown = {
{ text = 'Associations', event = 'associate' },
} },
}
}
end

View File

@@ -1,21 +1,18 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
--[[-- DropMenuItem --]]--
UI.DropMenuItem = class(UI.Button) UI.DropMenuItem = class(UI.Button)
UI.DropMenuItem.defaults = { UI.DropMenuItem.defaults = {
UIElement = 'DropMenuItem', UIElement = 'DropMenuItem',
textColor = colors.black, textColor = 'black',
backgroundColor = colors.white, backgroundColor = 'white',
textFocusColor = colors.white, textFocusColor = 'white',
textInactiveColor = colors.lightGray, textInactiveColor = 'lightGray',
backgroundFocusColor = colors.lightGray, backgroundFocusColor = 'lightGray',
} }
function UI.DropMenuItem:eventHandler(event) function UI.DropMenuItem:eventHandler(event)
if event.type == 'button_activate' then if event.type == 'button_activate' then
self.parent:hide() self.parent:disable()
end end
return UI.Button.eventHandler(self, event) return UI.Button.eventHandler(self, event)
end end

View File

@@ -1,62 +1,63 @@
local class = require('opus.class') local class = require('opus.class')
local Event = require('opus.event')
local Terminal = require('opus.terminal') local Terminal = require('opus.terminal')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Embedded = class(UI.Window) UI.Embedded = class(UI.Window)
UI.Embedded.defaults = { UI.Embedded.defaults = {
UIElement = 'Embedded', UIElement = 'Embedded',
backgroundColor = colors.black, backgroundColor = 'black',
textColor = colors.white, textColor = 'white',
maxScroll = 100, maxScroll = 100,
accelerators = { accelerators = {
up = 'scroll_up', up = 'scroll_up',
down = 'scroll_down', down = 'scroll_down',
} }
} }
function UI.Embedded:setParent()
UI.Window.setParent(self)
self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false)
self.win.setMaxScroll(self.maxScroll)
local canvas = self:getCanvas()
self.win.getCanvas().parent = canvas
table.insert(canvas.layers, self.win.getCanvas())
self.canvas = self.win.getCanvas()
self.win.setCursorPos(1, 1)
self.win.setBackgroundColor(self.backgroundColor)
self.win.setTextColor(self.textColor)
self.win.clear()
end
function UI.Embedded:layout() function UI.Embedded:layout()
UI.Window.layout(self) UI.Window.layout(self)
if self.win then
self.win.reposition(self.x, self.y, self.width, self.height) if not self.win then
local t
function self.render()
if not t then
t = Event.onTimeout(0, function()
t = nil
if self.focused then
self:setCursorPos(self.win.getCursorPos())
end
self:sync()
end)
end
end
self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false)
self.win.canvas = self
self.win.setMaxScroll(self.maxScroll)
self.win.setCursorPos(1, 1)
self.win.setBackgroundColor(self.backgroundColor)
self.win.setTextColor(self.textColor)
self.win.clear()
end end
end end
function UI.Embedded:draw() function UI.Embedded:draw()
self.canvas:dirty() self:dirty()
end
function UI.Embedded:focus()
-- allow scrolling
if self.focused then
self:setCursorBlink(self.win.getCursorBlink())
end
end end
function UI.Embedded:enable() function UI.Embedded:enable()
self.canvas:setVisible(true)
self.canvas:raise()
if self.visible then
-- the window will automatically update on changes
-- the canvas does not need to be rendereed
self.win.setVisible(true)
end
UI.Window.enable(self) UI.Window.enable(self)
self.canvas:dirty() self.win.setVisible(true)
self:dirty()
end end
function UI.Embedded:disable() function UI.Embedded:disable()
self.canvas:setVisible(false)
self.win.setVisible(false) self.win.setVisible(false)
UI.Window.disable(self) UI.Window.disable(self)
end end
@@ -71,6 +72,25 @@ function UI.Embedded:eventHandler(event)
end end
end end
function UI.Embedded:focus() function UI.Embedded.example()
-- allow scrolling local Util = require('opus.util')
local term = _G.term
return UI.Embedded {
y = 2, x = 2, ex = -2, ey = -2,
enable = function (self)
UI.Embedded.enable(self)
Event.addRoutine(function()
local oterm = term.redirect(self.win)
Util.run(_ENV, '/sys/apps/shell.lua')
term.redirect(oterm)
end)
end,
eventHandler = function(self, event)
if event.type == 'key' then
return true
end
return UI.Embedded.eventHandler(self, event)
end
}
end end

View File

@@ -0,0 +1,118 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local fs = _G.fs
UI.FileSelect = class(UI.Window)
UI.FileSelect.defaults = {
UIElement = 'FileSelect',
}
function UI.FileSelect:postInit()
self.grid = UI.ScrollingGrid {
x = 2, y = 2, ex = -2, ey = -4,
dir = '/',
sortColumn = 'name',
columns = {
{ heading = 'Name', key = 'name' },
{ heading = 'Size', key = 'size', width = 5 }
},
getDisplayValues = function(_, row)
if row.size then
row = Util.shallowCopy(row)
row.size = Util.toBytes(row.size)
end
return row
end,
getRowTextColor = function(_, file)
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end,
sortCompare = function(self, a, b)
if self.sortColumn == 'size' then
return a.size < b.size
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end,
draw = function(self)
local files = fs.listEx(self.dir)
if #self.dir > 0 then
table.insert(files, {
name = '..',
isDir = true,
})
end
self:setValues(files)
self:setIndex(1)
UI.Grid.draw(self)
end,
}
self.path = UI.TextEntry {
x = 2,
y = -2,
ex = -11,
limit = 256,
accelerators = {
enter = 'path_enter',
}
}
self.cancel = UI.Button {
text = 'Cancel',
x = -9,
y = -2,
event = 'select_cancel',
}
end
function UI.FileSelect:draw()
self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray)
self:drawChildren()
end
function UI.FileSelect:enable(path)
self:setPath(path or '')
UI.Window.enable(self)
end
function UI.FileSelect:setPath(path)
self.grid.dir = path
while not fs.isDir(self.grid.dir) do
self.grid.dir = fs.getDir(self.grid.dir)
end
self.path.value = self.grid.dir
end
function UI.FileSelect:eventHandler(event)
if event.type == 'grid_select' then
self.grid.dir = fs.combine(self.grid.dir, event.selected.name)
self.path.value = self.grid.dir
if event.selected.isDir then
self.grid:draw()
self.path:draw()
else
self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self })
end
return true
elseif event.type == 'path_enter' then
if self.path.value then
if fs.isDir(self.path.value) then
self:setPath(self.path.value)
self.grid:draw()
self.path:draw()
else
self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self })
end
end
return true
end
end

View File

@@ -0,0 +1,16 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.FlatButton = class(UI.Button)
UI.FlatButton.defaults = {
UIElement = 'FlatButton',
textColor = 'black',
textFocusColor = 'white',
noPadding = true,
}
function UI.FlatButton:setParent()
self.backgroundColor = self.parent:getProperty('backgroundColor')
self.backgroundFocusColor = self.backgroundColor
UI.Button.setParent(self)
end

View File

@@ -2,8 +2,6 @@ local class = require('opus.class')
local Sound = require('opus.sound') local Sound = require('opus.sound')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.Form = class(UI.Window) UI.Form = class(UI.Window)
UI.Form.defaults = { UI.Form.defaults = {
UIElement = 'Form', UIElement = 'Form',
@@ -32,7 +30,7 @@ function UI.Form:setValues(values)
if child.setValue then if child.setValue then
child:setValue(self.values[child.formKey]) child:setValue(self.values[child.formKey])
else else
child.value = self.values[child.formKey] or '' child.value = self.values[child.formKey]
end end
end end
end end
@@ -56,7 +54,7 @@ function UI.Form:createForm()
for _, child in pairs(self) do for _, child in pairs(self) do
if type(child) == 'table' and child.UIElement then if type(child) == 'table' and child.UIElement then
if child.formKey then if child.formKey then
child.value = self.values[child.formKey] or '' child.value = self.values[child.formKey]
end end
if child.formLabel then if child.formLabel then
child.x = self.labelWidth + self.margin - 1 child.x = self.labelWidth + self.margin - 1
@@ -68,7 +66,7 @@ function UI.Form:createForm()
table.insert(self.children, UI.Text { table.insert(self.children, UI.Text {
x = self.margin, x = self.margin,
y = child.y, y = child.y,
textColor = colors.black, textColor = 'black',
width = #child.formLabel, width = #child.formLabel,
value = child.formLabel, value = child.formLabel,
}) })
@@ -99,14 +97,6 @@ function UI.Form:validateField(field)
return false, 'Field is required' return false, 'Field is required'
end end
end end
if field.validate == 'numeric' then
field.value = field.value or ''
if #tostring(field.value) > 0 then
if not tonumber(field.value) then
return false, 'Invalid number'
end
end
end
return true return true
end end
@@ -124,11 +114,7 @@ function UI.Form:save()
end end
for _,child in pairs(self.children) do for _,child in pairs(self.children) do
if child.formKey then if child.formKey then
if child.validate == 'numeric' then self.values[child.formKey] = child.value
self.values[child.formKey] = tonumber(child.value)
else
self.values[child.formKey] = child.value
end
end end
end end
@@ -146,3 +132,23 @@ function UI.Form:eventHandler(event)
end end
return true return true
end end
function UI.Form.example()
return UI.Form {
x = 2, ex = -2, y = 2,
ptype = UI.Chooser {
formLabel = 'Type', formKey = 'type', formIndex = 1,
width = 10,
choices = {
{ name = 'Modem', value = 'wireless_modem' },
{ name = 'Drive', value = 'disk_drive' },
},
},
drive_id = UI.TextEntry {
formLabel = 'Drive', formKey = 'drive_id', formIndex = 2,
required = true,
width = 5,
transform = 'number',
},
}
end

View File

@@ -2,10 +2,8 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local os = _G.os local os = _G.os
local _rep = string.rep local _rep = string.rep
local _sub = string.sub
local function safeValue(v) local function safeValue(v)
local t = type(v) local t = type(v)
@@ -23,18 +21,7 @@ function Writer:init(element, y)
end end
function Writer:write(s, width, align, bg, fg) function Writer:write(s, width, align, bg, fg)
local len = #tostring(s or '') s = Util.widthify(s, width, align)
if len > width then
s = _sub(s, 1, width)
end
local padding = len < width and _rep(' ', width - len)
if padding then
if align == 'right' then
s = padding .. s
else
s = s .. padding
end
end
self.element:write(self.x, self.y, s, bg, fg) self.element:write(self.x, self.y, s, bg, fg)
self.x = self.x + width self.x = self.x + width
end end
@@ -56,16 +43,16 @@ UI.Grid.defaults = {
disableHeader = false, disableHeader = false,
headerHeight = 1, headerHeight = 1,
marginRight = 0, marginRight = 0,
textColor = colors.white, textColor = 'white',
textSelectedColor = colors.white, textSelectedColor = 'white',
backgroundColor = colors.black, backgroundColor = 'black',
backgroundSelectedColor = colors.gray, backgroundSelectedColor = 'gray',
headerBackgroundColor = colors.cyan, headerBackgroundColor = 'primary',
headerTextColor = colors.white, headerTextColor = 'white',
headerSortColor = colors.yellow, headerSortColor = 'yellow',
unfocusedTextSelectedColor = colors.white, unfocusedTextSelectedColor = 'white',
unfocusedBackgroundSelectedColor = colors.gray, unfocusedBackgroundSelectedColor = 'gray',
focusIndicator = UI.extChars and '\183' or '>', focusIndicator = UI.extChars and '\26' or '>',
sortIndicator = ' ', sortIndicator = ' ',
inverseSortIndicator = UI.extChars and '\24' or '^', inverseSortIndicator = UI.extChars and '\24' or '^',
values = { }, values = { },
@@ -83,8 +70,8 @@ UI.Grid.defaults = {
[ 'control-f' ] = 'scroll_pageDown', [ 'control-f' ] = 'scroll_pageDown',
}, },
} }
function UI.Grid:setParent() function UI.Grid:layout()
UI.Window.setParent(self) UI.Window.layout(self)
for _,c in pairs(self.columns) do for _,c in pairs(self.columns) do
c.cw = c.width c.cw = c.width
@@ -493,3 +480,46 @@ function UI.Grid:eventHandler(event)
end end
return true return true
end end
function UI.Grid.example()
local values = {
{ key = 'key1', value = 'value1' },
{ key = 'key2', value = 'value2' },
{ key = 'key3', value = 'value3-longer value text' },
{ key = 'key4', value = 'value4' },
{ key = 'key5', value = 'value5' },
}
return UI.Window {
regular = UI.Grid {
ex = '48%', ey = 4,
values = values,
sortColumn = 'key',
inverseSort = true,
columns = {
{ heading = 'key', key = 'key' },
{ heading = 'value', key = 'value' },
},
accelerators = {
grid_select = 'custom_select',
}
},
noheader = UI.Grid {
ex = '48%', y = 6, ey = -2,
disableHeader = true,
values = values,
columns = {
{ heading = 'key', key = 'key', width = 6, },
{ heading = 'value', key = 'value', textColor = 'yellow' },
},
},
autospace = UI.Grid {
x = '52%', ey = 4,
autospace = true,
values = values,
columns = {
{ heading = 'key', key = 'key' },
{ heading = 'value', key = 'value' },
},
},
}
end

View File

@@ -1,19 +1,28 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util')
local lookup = '0123456789abcdef'
-- handle files produced by Paint
UI.Image = class(UI.Window) UI.Image = class(UI.Window)
UI.Image.defaults = { UI.Image.defaults = {
UIElement = 'Image', UIElement = 'Image',
event = 'button_press', event = 'button_press',
} }
function UI.Image:setParent() function UI.Image:postInit()
if self.image then if self.filename then
self.image = Util.readLines(self.filename)
end
if self.image and not (self.height or self.ey) then
self.height = #self.image self.height = #self.image
end end
if self.image and not self.width then if self.image and not (self.width or self.ex) then
self.width = #self.image[1] for i = 1, self.height do
self.width = math.max(self.width or 0, #self.image[i])
end
end end
UI.Window.setParent(self)
end end
function UI.Image:draw() function UI.Image:draw()
@@ -22,19 +31,22 @@ function UI.Image:draw()
for y = 1, #self.image do for y = 1, #self.image do
local line = self.image[y] local line = self.image[y]
for x = 1, #line do for x = 1, #line do
local ch = line[x] local ch = lookup:find(line:sub(x, x))
if type(ch) == 'number' then if ch then
if ch > 0 then self:write(x, y, ' ', 2 ^ (ch -1))
self:write(x, y, ' ', ch)
end
else
self:write(x, y, ch)
end end
end end
end end
end end
self:drawChildren()
end end
function UI.Image:setImage(image) function UI.Image:setImage(image)
self.image = image self.image = image
end end
function UI.Image.example()
return UI.Image {
filename = 'test.paint',
}
end

View File

@@ -1,7 +1,6 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
--[[-- Menu --]]--
UI.Menu = class(UI.Grid) UI.Menu = class(UI.Grid)
UI.Menu.defaults = { UI.Menu.defaults = {
UIElement = 'Menu', UIElement = 'Menu',
@@ -14,8 +13,7 @@ function UI.Menu:postInit()
self.pageSize = #self.menuItems self.pageSize = #self.menuItems
end end
function UI.Menu:setParent() function UI.Menu:layout()
UI.Grid.setParent(self)
self.itemWidth = 1 self.itemWidth = 1
for _,v in pairs(self.values) do for _,v in pairs(self.values) do
if #v.prompt > self.itemWidth then if #v.prompt > self.itemWidth then
@@ -29,6 +27,7 @@ function UI.Menu:setParent()
else else
self.width = self.itemWidth + 2 self.width = self.itemWidth + 2
end end
UI.Grid.layout(self)
end end
function UI.Menu:center() function UI.Menu:center()
@@ -59,3 +58,14 @@ function UI.Menu:eventHandler(event)
end end
return UI.Grid.eventHandler(self, event) return UI.Grid.eventHandler(self, event)
end end
function UI.Menu.example()
return UI.Menu {
x = 2, y = 2, height = 3,
menuItems = {
{ prompt = 'Start', event = 'start' },
{ prompt = 'Continue', event = 'continue' },
{ prompt = 'Quit', event = 'quit' }
}
}
end

View File

@@ -1,28 +1,15 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local function getPosition(element)
local x, y = 1, 1
repeat
x = element.x + x - 1
y = element.y + y - 1
element = element.parent
until not element
return x, y
end
UI.MenuBar = class(UI.Window) UI.MenuBar = class(UI.Window)
UI.MenuBar.defaults = { UI.MenuBar.defaults = {
UIElement = 'MenuBar', UIElement = 'MenuBar',
buttons = { }, buttons = { },
height = 1, height = 1,
backgroundColor = colors.lightGray, backgroundColor = 'secondary',
textColor = colors.black, textColor = 'black',
spacing = 2, spacing = 2,
lastx = 1, lastx = 1,
showBackButton = false,
buttonClass = 'MenuItem', buttonClass = 'MenuItem',
} }
function UI.MenuBar:postInit() function UI.MenuBar:postInit()
@@ -34,6 +21,15 @@ function UI.MenuBar:addButtons(buttons)
self.children = { } self.children = { }
end end
for _,button in pairs(buttons) do
if button.index then -- don't sort unless needed
table.sort(buttons, function(a, b)
return (a.index or 999) < (b.index or 999)
end)
break
end
end
for _,button in pairs(buttons) do for _,button in pairs(buttons) do
if button.UIElement then if button.UIElement then
table.insert(self.children, button) table.insert(self.children, button)
@@ -42,6 +38,7 @@ function UI.MenuBar:addButtons(buttons)
x = self.lastx, x = self.lastx,
width = #(button.text or 'button') + self.spacing, width = #(button.text or 'button') + self.spacing,
centered = false, centered = false,
backgroundColor = self.backgroundColor,
} }
self.lastx = self.lastx + buttonProperties.width self.lastx = self.lastx + buttonProperties.width
UI:mergeProperties(buttonProperties, button) UI:mergeProperties(buttonProperties, button)
@@ -52,10 +49,6 @@ function UI.MenuBar:addButtons(buttons)
else else
table.insert(self.children, button) table.insert(self.children, button)
end end
if button.dropdown then
button.dropmenu = UI.DropMenu { buttons = button.dropdown }
end
end end
end end
if self.parent then if self.parent then
@@ -68,23 +61,38 @@ function UI.MenuBar:getActive(menuItem)
end end
function UI.MenuBar:eventHandler(event) function UI.MenuBar:eventHandler(event)
if event.type == 'button_press' and event.button.dropmenu then if event.type == 'button_press' and event.button.dropdown then
if event.button.dropmenu.enabled then local function getPosition(element)
event.button.dropmenu:hide() local x, y = 1, 1
self:refocus() repeat
return true x = element.x + x - 1
else y = element.y + y - 1
local x, y = getPosition(event.button) element = element.parent
if x + event.button.dropmenu.width > self.width then until not element
x = self.width - event.button.dropmenu.width + 1 return x, y
end
for _,c in pairs(event.button.dropmenu.children) do
if not c.spacer then
c.inactive = not self:getActive(c)
end
end
event.button.dropmenu:show(x, y + 1)
end end
local x, y = getPosition(event.button)
local menu = UI.DropMenu {
buttons = event.button.dropdown,
x = x,
y = y + 1,
lastFocus = event.button.uid,
menuUid = self.uid,
}
self.parent:add({ dropmenu = menu })
return true return true
end end
end end
function UI.MenuBar.example()
return UI.MenuBar {
buttons = {
{ text = 'Choice1', event = 'event1' },
{ text = 'Choice2', event = 'event2', inactive = true },
{ text = 'Choice3', event = 'event3' },
}
}
end

View File

@@ -1,14 +1,9 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors UI.MenuItem = class(UI.FlatButton)
--[[-- MenuItem --]]--
UI.MenuItem = class(UI.Button)
UI.MenuItem.defaults = { UI.MenuItem.defaults = {
UIElement = 'MenuItem', UIElement = 'MenuItem',
textColor = colors.black, noPadding = false,
backgroundColor = colors.lightGray, textInactiveColor = 'gray',
textFocusColor = colors.white,
backgroundFocusColor = colors.lightGray,
} }

View File

@@ -0,0 +1,31 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.MiniSlideOut = class(UI.SlideOut)
UI.MiniSlideOut.defaults = {
UIElement = 'MiniSlideOut',
noFill = true,
backgroundColor = 'primary',
height = 1,
}
function UI.MiniSlideOut:postInit()
self.close_button = UI.Button {
x = -1,
backgroundColor = self.backgroundColor,
backgroundFocusColor = self.backgroundColor,
text = 'x',
event = 'slide_hide',
noPadding = true,
}
if self.label then
self.label_text = UI.Text {
x = 2,
value = self.label,
}
end
end
function UI.MiniSlideOut:show(...)
UI.SlideOut.show(self, ...)
self:addTransition('slideLeft', { easing = 'outBounce' })
end

View File

@@ -5,17 +5,18 @@ UI.NftImage = class(UI.Window)
UI.NftImage.defaults = { UI.NftImage.defaults = {
UIElement = 'NftImage', UIElement = 'NftImage',
} }
function UI.NftImage:setParent() function UI.NftImage:postInit()
if self.image then if self.image and not (self.ey or self.height) then
self.height = self.image.height self.height = self.image.height
end end
if self.image and not self.width then if self.image and not (self.ex or self.width) then
self.width = self.image.width self.width = self.image.width
end end
UI.Window.setParent(self)
end end
function UI.NftImage:draw() function UI.NftImage:draw()
self:clear()
if self.image then if self.image then
-- due to blittle, the background and foreground transparent -- due to blittle, the background and foreground transparent
-- color is the same as the background color -- color is the same as the background color
@@ -25,8 +26,6 @@ function UI.NftImage:draw()
self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg) self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg)
end end
end end
else
self:clear()
end end
end end

View File

@@ -4,36 +4,34 @@ local Sound = require('opus.sound')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.Notification = class(UI.Window) UI.Notification = class(UI.Window)
UI.Notification.defaults = { UI.Notification.defaults = {
UIElement = 'Notification', UIElement = 'Notification',
backgroundColor = colors.gray, backgroundColor = 'gray',
closeInd = UI.extChars and '\215' or '*', closeInd = UI.extChars and '\215' or '*',
height = 3, height = 3,
timeout = 3, timeout = 3,
anchor = 'bottom', anchor = 'bottom',
} }
function UI.Notification:draw() function UI.Notification.draw()
end end
function UI.Notification:enable() function UI.Notification.enable()
end end
function UI.Notification:error(value, timeout) function UI.Notification:error(value, timeout)
self.backgroundColor = colors.red self.backgroundColor = 'red'
Sound.play('entity.villager.no', .5) Sound.play('entity.villager.no', .5)
self:display(value, timeout) self:display(value, timeout)
end end
function UI.Notification:info(value, timeout) function UI.Notification:info(value, timeout)
self.backgroundColor = colors.lightGray self.backgroundColor = 'lightGray'
self:display(value, timeout) self:display(value, timeout)
end end
function UI.Notification:success(value, timeout) function UI.Notification:success(value, timeout)
self.backgroundColor = colors.green self.backgroundColor = 'green'
self:display(value, timeout) self:display(value, timeout)
end end
@@ -43,32 +41,34 @@ function UI.Notification:cancel()
self.timer = nil self.timer = nil
end end
if self.canvas then self:disable()
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
end
end end
function UI.Notification:display(value, timeout) function UI.Notification:display(value, timeout)
self:cancel()
self.enabled = true
local lines = Util.wordWrap(value, self.width - 3) local lines = Util.wordWrap(value, self.width - 3)
self.enabled = true
self.height = #lines self.height = #lines
if self.anchor == 'bottom' then if self.anchor == 'bottom' then
self.y = self.parent.height - self.height + 1 self.y = self.parent.height - self.height + 1
self.canvas = self:addLayer(self.backgroundColor, self.textColor)
self:addTransition('expandUp', { ticks = self.height }) self:addTransition('expandUp', { ticks = self.height })
else else
self.canvas = self:addLayer(self.backgroundColor, self.textColor)
self.y = 1 self.y = 1
end end
self.canvas:setVisible(true)
self:reposition(self.x, self.y, self.width, self.height)
self:raise()
self:clear() self:clear()
for k,v in pairs(lines) do for k,v in pairs(lines) do
self:write(2, k, v) self:write(2, k, v)
end end
self:write(self.width, 1, self.closeInd)
if self.timer then
Event.off(self.timer)
self.timer = nil
end
timeout = timeout or self.timeout timeout = timeout or self.timeout
if timeout > 0 then if timeout > 0 then
@@ -77,7 +77,6 @@ function UI.Notification:display(value, timeout)
self:sync() self:sync()
end) end)
else else
self:write(self.width, 1, self.closeInd)
self:sync() self:sync()
end end
end end
@@ -90,3 +89,31 @@ function UI.Notification:eventHandler(event)
end end
end end
end end
function UI.Notification.example()
return UI.Window {
notify1 = UI.Notification {
anchor = 'top',
},
notify2 = UI.Notification { },
button1 = UI.Button {
x = 2, y = 3,
text = 'example 1',
event = 'test_success',
},
button2 = UI.Button {
x = 2, y = 5,
text = 'example 2',
event = 'test_error',
},
eventHandler = function (self, event)
if event.type == 'test_success' then
self.notify1:success('Example text')
elseif event.type == 'test_error' then
self.notify2:error([[Example text test test
test test test test test
test test test]], 0)
end
end,
}
end

View File

@@ -0,0 +1,145 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
UI.Page = class(UI.Window)
UI.Page.defaults = {
UIElement = 'Page',
accelerators = {
down = 'focus_next',
scroll_down = 'focus_next',
enter = 'focus_next',
tab = 'focus_next',
['shift-tab' ] = 'focus_prev',
up = 'focus_prev',
scroll_up = 'focus_prev',
},
backgroundColor = 'primary',
textColor = 'white',
}
function UI.Page:postInit()
self.parent = self.parent or UI.term
self.__target = self
end
function UI.Page:sync()
if self.enabled then
self:checkFocus()
self.parent:setCursorBlink(self.focused and self.focused.cursorBlink)
self.parent:sync()
end
end
function UI.Page:capture(child)
self.__target = child
end
function UI.Page:release(child)
if self.__target == child then
self.__target = self
end
end
function UI.Page:pointToChild(x, y)
if self.__target == self then
return UI.Window.pointToChild(self, x, y)
end
local function getPosition(element)
local x, y = 1, 1
repeat
x = element.x + x - 1
y = element.y + y - 1
element = element.parent
until not element
return x, y
end
local absX, absY = getPosition(self.__target)
return self.__target:pointToChild(x - absX + self.__target.x, y - absY + self.__target.y)
end
function UI.Page:getFocusables()
if self.__target == self or not self.__target.modal then
return UI.Window.getFocusables(self)
end
return self.__target:getFocusables()
end
function UI.Page:getFocused()
return self.focused
end
function UI.Page:focusPrevious()
local function getPreviousFocus(focused)
local focusables = self:getFocusables()
local k = Util.contains(focusables, focused)
if k then
if k > 1 then
return focusables[k - 1]
end
return focusables[#focusables]
end
end
local focused = getPreviousFocus(self.focused)
if focused then
self:setFocus(focused)
end
end
function UI.Page:focusNext()
local function getNextFocus(focused)
local focusables = self:getFocusables()
local k = Util.contains(focusables, focused)
if k then
if k < #focusables then
return focusables[k + 1]
end
return focusables[1]
end
end
local focused = getNextFocus(self.focused)
if focused then
self:setFocus(focused)
end
end
function UI.Page:setFocus(child)
if not child or not child.focus then
return
end
if self.focused and self.focused ~= child then
self.focused.focused = false
self.focused:focus()
self.focused:emit({ type = 'focus_lost', focused = child, unfocused = self.focused })
end
self.focused = child
if not child.focused then
child.focused = true
child:emit({ type = 'focus_change', focused = child })
end
child:focus()
end
function UI.Page:checkFocus()
if not self.focused or not self.focused.enabled then
self.__target:focusFirst()
end
end
function UI.Page:eventHandler(event)
if self.focused then
if event.type == 'focus_next' then
self:focusNext()
return true
elseif event.type == 'focus_prev' then
self:focusPrevious()
return true
end
end
end

View File

@@ -1,28 +1,36 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.ProgressBar = class(UI.Window) UI.ProgressBar = class(UI.Window)
UI.ProgressBar.defaults = { UI.ProgressBar.defaults = {
UIElement = 'ProgressBar', UIElement = 'ProgressBar',
backgroundColor = colors.gray, backgroundColor = 'gray',
height = 1, height = 1,
progressColor = colors.lime, progressColor = 'lime',
progressChar = UI.extChars and '\153' or ' ', progressChar = UI.extChars and '\153' or ' ',
fillChar = ' ', fillChar = ' ',
fillColor = colors.gray, fillColor = 'gray',
textColor = colors.green, textColor = 'green',
value = 0, value = 0,
} }
function UI.ProgressBar:draw() function UI.ProgressBar:draw()
local width = math.ceil(self.value / 100 * self.width) local width = math.ceil(self.value / 100 * self.width)
local filler = string.rep(self.fillChar, self.width) self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor)
local progress = string.rep(self.progressChar, width) self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor)
end
for i = 1, self.height do
self:write(1, i, filler, nil, self.fillColor) function UI.ProgressBar.example()
self:write(1, i, progress, self.progressColor) return UI.ProgressBar {
end x = 2, ex = -2, y = 2, height = 2,
focus = function() end,
enable = function(self)
require('opus.event').onInterval(.25, function()
self.value = self.value == 100 and 0 or self.value + 5
self:draw()
self:sync()
end)
return UI.ProgressBar.enable(self)
end
}
end end

View File

@@ -0,0 +1,27 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.Question = class(UI.MiniSlideOut)
UI.Question.defaults = {
UIElement = 'Question',
accelerators = {
y = 'question_yes',
n = 'question_no',
}
}
function UI.Question:postInit()
local x = self.label and #self.label + 3 or 1
self.yes_button = UI.Button {
x = x,
text = 'Yes',
backgroundColor = 'primary',
event = 'question_yes',
}
self.no_button = UI.Button {
x = x + 5,
text = 'No',
backgroundColor = 'primary',
event = 'question_no',
}
end

View File

@@ -17,7 +17,13 @@ UI.ScrollBar.defaults = {
ey = -1, ey = -1,
} }
function UI.ScrollBar:draw() function UI.ScrollBar:draw()
local view = self.parent:getViewArea() local parent = self.target or self.parent --self:find(self.target)
local view = parent:getViewArea()
self:clear()
-- ...
self:write(1, 1, ' ', view.fill)
if view.totalHeight > view.height then if view.totalHeight > view.height then
local maxScroll = view.totalHeight - view.height local maxScroll = view.totalHeight - view.height
@@ -27,7 +33,7 @@ function UI.ScrollBar:draw()
local row = view.y local row = view.y
if not view.static then -- does the container scroll ? if not view.static then -- does the container scroll ?
self.height = view.totalHeight self:reposition(self.x, self.y, self.width, view.totalHeight)
end end
for i = 1, view.height - 2 do for i = 1, view.height - 2 do
@@ -56,16 +62,17 @@ end
function UI.ScrollBar:eventHandler(event) function UI.ScrollBar:eventHandler(event)
if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
if event.x == 1 then if event.x == 1 then
local view = self.parent:getViewArea() local parent = self.target or self.parent --self:find(self.target)
local view = parent:getViewArea()
if view.totalHeight > view.height then if view.totalHeight > view.height then
if event.y == view.y then if event.y == view.y then
self:emit({ type = 'scroll_up'}) parent:emit({ type = 'scroll_up'})
elseif event.y == view.y + view.height - 1 then elseif event.y == view.y + view.height - 1 then
self:emit({ type = 'scroll_down'}) parent:emit({ type = 'scroll_down'})
else else
local percent = (event.y - view.y) / (view.height - 2) local percent = (event.y - view.y) / (view.height - 2)
local y = math.floor((view.totalHeight - view.height) * percent) local y = math.floor((view.totalHeight - view.height) * percent)
self:emit({ type = 'scroll_to', offset = y }) parent :emit({ type = 'scroll_to', offset = y })
end end
end end
return true return true

View File

@@ -2,7 +2,6 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
--[[-- ScrollingGrid --]]--
UI.ScrollingGrid = class(UI.Grid) UI.ScrollingGrid = class(UI.Grid)
UI.ScrollingGrid.defaults = { UI.ScrollingGrid.defaults = {
UIElement = 'ScrollingGrid', UIElement = 'ScrollingGrid',
@@ -30,6 +29,7 @@ function UI.ScrollingGrid:getViewArea()
height = self.pageSize, -- viewable height height = self.pageSize, -- viewable height
totalHeight = Util.size(self.values), -- total height totalHeight = Util.size(self.values), -- total height
offsetY = self.scrollOffset, -- scroll offset offsetY = self.scrollOffset, -- scroll offset
fill = not self.disableHeader and self.headerBackgroundColor,
} }
end end
@@ -58,3 +58,21 @@ function UI.ScrollingGrid:setIndex(index)
end end
UI.Grid.setIndex(self, index) UI.Grid.setIndex(self, index)
end end
function UI.ScrollingGrid.example()
local values = { }
for i = 1, 20 do
table.insert(values, { key = 'key' .. i, value = 'value' .. i })
end
return UI.ScrollingGrid {
values = values,
sortColumn = 'key',
columns = {
{ heading = 'key', key = 'key' },
{ heading = 'value', key = 'value' },
},
accelerators = {
grid_select = 'custom_select',
}
}
end

View File

@@ -1,43 +1,38 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
--[[-- SlideOut --]]--
UI.SlideOut = class(UI.Window) UI.SlideOut = class(UI.Window)
UI.SlideOut.defaults = { UI.SlideOut.defaults = {
UIElement = 'SlideOut', UIElement = 'SlideOut',
pageType = 'modal', transitionHint = 'expandUp',
modal = true,
} }
function UI.SlideOut:layout()
UI.Window.layout(self)
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.SlideOut:enable() function UI.SlideOut:enable()
end end
function UI.SlideOut:show(...) function UI.SlideOut:toggle()
self:addTransition('expandUp') if self.enabled then
self.canvas:raise() self:hide()
self.canvas:setVisible(true) else
UI.Window.enable(self, ...) self:show()
self:draw() end
self:capture(self)
self:focusFirst()
end end
function UI.SlideOut:disable() function UI.SlideOut:show(...)
self.canvas:setVisible(false) UI.Window.enable(self, ...)
UI.Window.disable(self) self:draw()
self:focusFirst()
end end
function UI.SlideOut:hide() function UI.SlideOut:hide()
self:disable() self:disable()
self:release(self) end
self:refocus()
function UI.SlideOut:draw()
if not self.noFill then
self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), 'black', 'gray')
end
self:drawChildren()
end end
function UI.SlideOut:eventHandler(event) function UI.SlideOut:eventHandler(event)
@@ -50,3 +45,31 @@ function UI.SlideOut:eventHandler(event)
return true return true
end end
end end
function UI.SlideOut.example()
return UI.Window {
y = 3,
backgroundColor = 2048,
button = UI.Button {
x = 2, y = 5,
text = 'show',
},
slideOut = UI.SlideOut {
backgroundColor = 16,
y = -7, height = 4, x = 3, ex = -3,
titleBar = UI.TitleBar {
title = 'test',
},
button = UI.Button {
x = 2, y = 2,
text = 'hide',
--visualize = true,
},
},
eventHandler = function (self, event)
if event.type == 'button_press' then
self.slideOut:toggle()
end
end,
}
end

View File

@@ -2,17 +2,15 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.Slider = class(UI.Window) UI.Slider = class(UI.Window)
UI.Slider.defaults = { UI.Slider.defaults = {
UIElement = 'Slider', UIElement = 'Slider',
height = 1, height = 1,
barChar = UI.extChars and '\140' or '-', barChar = UI.extChars and '\140' or '-',
barColor = colors.gray, barColor = 'gray',
sliderChar = UI.extChars and '\143' or '\124', sliderChar = UI.extChars and '\143' or '\124',
sliderColor = colors.blue, sliderColor = 'blue',
sliderFocusColor = colors.lightBlue, sliderFocusColor = 'lightBlue',
leftBorder = UI.extChars and '\141' or '\124', leftBorder = UI.extChars and '\141' or '\124',
rightBorder = UI.extChars and '\142' or '\124', rightBorder = UI.extChars and '\142' or '\124',
value = 0, value = 0,
@@ -49,7 +47,7 @@ function UI.Slider:draw()
i == self.width and self.rightBorder or i == self.width and self.rightBorder or
self.barChar self.barChar
table.insert(bar, filler) table.insert(bar, filler)
end end
self:write(1, 1, table.concat(bar), nil, self.barColor) self:write(1, 1, table.concat(bar), nil, self.barColor)
self:write(progress, 1, self.sliderChar, nil, self.focused and self.sliderFocusColor or self.sliderColor) self:write(progress, 1, self.sliderChar, nil, self.focused and self.sliderFocusColor or self.sliderColor)
@@ -57,8 +55,16 @@ end
function UI.Slider:eventHandler(event) function UI.Slider:eventHandler(event)
if event.type == "mouse_down" or event.type == "mouse_drag" then if event.type == "mouse_down" or event.type == "mouse_drag" then
local pos = event.x - 1
if event.type == 'mouse_down' then
self.anchor = event.x - 1
else
pos = self.anchor + event.dx
end
local range = self.max - self.min local range = self.max - self.min
local i = (event.x - 1) / (self.width - 1) local i = pos / (self.width - 1)
self.value = self.min + (i * range) self.value = self.min + (i * range)
self:emit({ type = self.event, value = self.value, element = self }) self:emit({ type = self.event, value = self.value, element = self })
self:draw() self:draw()
@@ -75,3 +81,10 @@ function UI.Slider:eventHandler(event)
self:draw() self:draw()
end end
end end
function UI.Slider.example()
return UI.Slider {
y = 2, x = 2, ex = -2,
min = 0, max = 1,
}
end

View File

@@ -3,17 +3,16 @@ local Event = require('opus.event')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.StatusBar = class(UI.Window) UI.StatusBar = class(UI.Window)
UI.StatusBar.defaults = { UI.StatusBar.defaults = {
UIElement = 'StatusBar', UIElement = 'StatusBar',
backgroundColor = colors.lightGray, backgroundColor = 'lightGray',
textColor = colors.gray, textColor = 'gray',
height = 1, height = 1,
ey = -1, ey = -1,
} }
function UI.StatusBar:adjustWidth() function UI.StatusBar:layout()
UI.Window.layout(self)
-- Can only have 1 adjustable width -- Can only have 1 adjustable width
if self.columns then if self.columns then
local w = self.width - #self.columns - 1 local w = self.width - #self.columns - 1
@@ -31,16 +30,6 @@ function UI.StatusBar:adjustWidth()
end end
end end
function UI.StatusBar:resize()
UI.Window.resize(self)
self:adjustWidth()
end
function UI.StatusBar:setParent()
UI.Window.setParent(self)
self:adjustWidth()
end
function UI.StatusBar:setStatus(status) function UI.StatusBar:setStatus(status)
if self.values ~= status then if self.values ~= status then
self.values = status self.values = status
@@ -63,7 +52,7 @@ end
function UI.StatusBar:timedStatus(status, timeout) function UI.StatusBar:timedStatus(status, timeout)
self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor) self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor)
Event.on(timeout or 3, function() Event.onTimeout(timeout or 3, function()
if self.enabled then if self.enabled then
self:draw() self:draw()
self:sync() self:sync()
@@ -89,10 +78,29 @@ function UI.StatusBar:draw()
elseif type(self.values) == 'string' then elseif type(self.values) == 'string' then
self:write(1, 1, Util.widthify(' ' .. self.values, self.width)) self:write(1, 1, Util.widthify(' ' .. self.values, self.width))
else else
local s = '' local x = 2
self:clear()
for _,c in ipairs(self.columns) do for _,c in ipairs(self.columns) do
s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw) local s = Util.widthify(tostring(self.values[c.key] or ''), c.cw)
self:write(x, 1, s, c.bg, c.fg)
x = x + c.cw + 1
end end
self:write(1, 1, Util.widthify(s, self.width))
end end
end end
function UI.StatusBar.example()
return UI.Window {
status1 = UI.StatusBar { values = 'standard' },
status2 = UI.StatusBar {
ey = -3,
columns = {
{ key = 'field1' },
{ key = 'field2', width = 6 },
},
values = {
field1 = 'test',
field2 = '42',
}
}
}
end

View File

@@ -1,12 +1,16 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors UI.Tab = class(UI.Window)
UI.Tab = class(UI.ActiveLayer)
UI.Tab.defaults = { UI.Tab.defaults = {
UIElement = 'Tab', UIElement = 'Tab',
tabTitle = 'tab', tabTitle = 'tab',
backgroundColor = colors.cyan,
y = 2, y = 2,
} }
function UI.Tab:draw()
if not self.noFill then
self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray)
end
self:drawChildren()
end

View File

@@ -2,13 +2,13 @@ local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
UI.TabBar = class(UI.MenuBar) UI.TabBar = class(UI.MenuBar)
UI.TabBar.defaults = { UI.TabBar.defaults = {
UIElement = 'TabBar', UIElement = 'TabBar',
buttonClass = 'TabBarMenuItem', buttonClass = 'TabBarMenuItem',
selectedBackgroundColor = colors.cyan, backgroundColor = 'black',
selectedBackgroundColor = 'primary',
unselectedBackgroundColor = 'tertiary',
} }
function UI.TabBar:enable() function UI.TabBar:enable()
UI.MenuBar.enable(self) UI.MenuBar.enable(self)
@@ -32,7 +32,7 @@ function UI.TabBar:eventHandler(event)
self:emit({ type = 'tab_change', current = si, last = pi, tab = selected }) self:emit({ type = 'tab_change', current = si, last = pi, tab = selected })
end end
end end
UI.MenuBar.draw(self) self:draw(self)
end end
return UI.MenuBar.eventHandler(self, event) return UI.MenuBar.eventHandler(self, event)
end end

View File

@@ -1,25 +1,19 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
--[[-- TabBarMenuItem --]]--
UI.TabBarMenuItem = class(UI.Button) UI.TabBarMenuItem = class(UI.Button)
UI.TabBarMenuItem.defaults = { UI.TabBarMenuItem.defaults = {
UIElement = 'TabBarMenuItem', UIElement = 'TabBarMenuItem',
event = 'tab_select', event = 'tab_select',
textColor = colors.black, textInactiveColor = 'lightGray',
selectedBackgroundColor = colors.cyan,
unselectedBackgroundColor = colors.lightGray,
backgroundColor = colors.lightGray,
} }
function UI.TabBarMenuItem:draw() function UI.TabBarMenuItem:draw()
if self.selected then if self.selected then
self.backgroundColor = self.selectedBackgroundColor self.backgroundColor = self:getProperty('selectedBackgroundColor')
self.backgroundFocusColor = self.selectedBackgroundColor self.backgroundFocusColor = self.backgroundColor
else else
self.backgroundColor = self.unselectedBackgroundColor self.backgroundColor = self:getProperty('unselectedBackgroundColor')
self.backgroundFocusColor = self.unselectedBackgroundColor self.backgroundFocusColor = self.backgroundColor
end end
UI.Button.draw(self) UI.Button.draw(self)
end end

View File

@@ -3,6 +3,7 @@ local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
UI.Tabs = class(UI.Window) UI.Tabs = class(UI.Window)
UI.Tabs.docs = { }
UI.Tabs.defaults = { UI.Tabs.defaults = {
UIElement = 'Tabs', UIElement = 'Tabs',
} }
@@ -16,6 +17,7 @@ function UI.Tabs:add(children)
if type(child) == 'table' and child.UIElement and child.tabTitle then if type(child) == 'table' and child.UIElement and child.tabTitle then
child.y = 2 child.y = 2
table.insert(buttons, { table.insert(buttons, {
index = child.index,
text = child.tabTitle, text = child.tabTitle,
event = 'tab_select', event = 'tab_select',
tabUid = child.uid, tabUid = child.uid,
@@ -32,10 +34,12 @@ function UI.Tabs:add(children)
end end
if self.parent then if self.parent then
return UI.Window.add(self, children) UI.Window.add(self, children)
end end
end end
UI.Tabs.docs.selectTab = [[selectTab(TAB)
Make to the passed tab active.]]
function UI.Tabs:selectTab(tab) function UI.Tabs:selectTab(tab)
local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid) local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid)
if menuItem then if menuItem then
@@ -52,12 +56,12 @@ end
function UI.Tabs:enable() function UI.Tabs:enable()
self.enabled = true self.enabled = true
self.transitionHint = nil
self.tabBar:enable() self.tabBar:enable()
local menuItem = Util.find(self.tabBar.children, 'selected', true) local menuItem = Util.find(self.tabBar.children, 'selected', true)
for _,child in pairs(self.children) do for child in self:eachChild() do
child.transitionHint = nil
if child.uid == menuItem.tabUid then if child.uid == menuItem.tabUid then
child:enable() child:enable()
self:emit({ type = 'tab_activate', activated = child }) self:emit({ type = 'tab_activate', activated = child })
@@ -70,14 +74,11 @@ end
function UI.Tabs:eventHandler(event) function UI.Tabs:eventHandler(event)
if event.type == 'tab_change' then if event.type == 'tab_change' then
local tab = self:find(event.tab.tabUid) local tab = self:find(event.tab.tabUid)
if event.current > event.last then local hint = event.current > event.last and 'slideLeft' or 'slideRight'
self.transitionHint = 'slideLeft'
else
self.transitionHint = 'slideRight'
end
for _,child in pairs(self.children) do for child in self:eachChild() do
if child.uid == event.tab.tabUid then if child.uid == event.tab.tabUid then
child.transitionHint = hint
child:enable() child:enable()
elseif child.tabTitle then elseif child.tabTitle then
child:disable() child:disable()
@@ -85,5 +86,40 @@ function UI.Tabs:eventHandler(event)
end end
self:emit({ type = 'tab_activate', activated = tab }) self:emit({ type = 'tab_activate', activated = tab })
tab:draw() tab:draw()
return true
end end
end end
function UI.Tabs.example()
return UI.Tabs {
tab1 = UI.Tab {
index = 1,
tabTitle = 'tab1',
entry = UI.TextEntry { y = 3, shadowText = 'text' },
},
tab2 = UI.Tab {
index = 2,
tabTitle = 'tab2',
subtabs = UI.Tabs {
x = 3, y = 2, ex = -3, ey = -2,
tab1 = UI.Tab {
index = 1,
tabTitle = 'tab4',
entry = UI.TextEntry { y = 3, shadowText = 'text' },
},
tab3 = UI.Tab {
index = 2,
tabTitle = 'tab5',
},
},
},
tab3 = UI.Tab {
index = 3,
tabTitle = 'tab3',
},
enable = function(self)
UI.Tabs.enable(self)
self:setActive(self.tab3, false)
end,
}
end

View File

@@ -8,11 +8,11 @@ UI.Text.defaults = {
value = '', value = '',
height = 1, height = 1,
} }
function UI.Text:setParent() function UI.Text:layout()
if not self.width and not self.ex then if not self.width and not self.ex then
self.width = #tostring(self.value) self.width = #tostring(self.value)
end end
UI.Window.setParent(self) UI.Window.layout(self)
end end
function UI.Text:draw() function UI.Text:draw()

View File

@@ -1,36 +1,49 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
--[[-- TextArea --]]--
UI.TextArea = class(UI.Viewport) UI.TextArea = class(UI.Viewport)
UI.TextArea.defaults = { UI.TextArea.defaults = {
UIElement = 'TextArea', UIElement = 'TextArea',
marginRight = 2, marginRight = 2,
value = '', value = '',
showScrollBar = true,
} }
function UI.TextArea:postInit()
self.scrollBar = UI.ScrollBar()
end
function UI.TextArea:setText(text) function UI.TextArea:setText(text)
self:reset() self:reset()
self.value = text self.value = text
self:draw() self:draw()
end end
function UI.TextArea:focus() function UI.TextArea.focus()
-- allow keyboard scrolling -- allow keyboard scrolling
end end
function UI.TextArea:draw() function UI.TextArea:draw()
self:clear() self:clear()
-- self:setCursorPos(1, 1)
self.cursorX, self.cursorY = 1, 1
self:print(self.value) self:print(self.value)
self:drawChildren()
for _,child in pairs(self.children) do
if child.enabled then
child:draw()
end
end
end end
function UI.TextArea.example()
local Ansi = require('opus.ansi')
return UI.Window {
backgroundColor = 2048,
t1 = UI.TextArea {
ey = 3,
value = 'sample text\nabc'
},
t2 = UI.TextArea {
y = 5,
backgroundColor = 'green',
value = string.format([[now %%is the %stime %sfor%s all good men to come to the aid of their country.
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
3
4
5
6
7
8]], Ansi.yellow, Ansi.onred, Ansi.reset),
}
}
end

View File

@@ -3,29 +3,37 @@ local entry = require('opus.entry')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local _rep = string.rep local _rep = string.rep
local _lower = string.lower
local _upper = string.upper local function transform(directive)
local transforms = {
lowercase = string.lower,
uppercase = string.upper,
number = tonumber,
}
return transforms[directive]
end
UI.TextEntry = class(UI.Window) UI.TextEntry = class(UI.Window)
UI.TextEntry.docs = { }
UI.TextEntry.defaults = { UI.TextEntry.defaults = {
UIElement = 'TextEntry', UIElement = 'TextEntry',
--value = '',
shadowText = '', shadowText = '',
focused = false, focused = false,
textColor = colors.white, textColor = 'white',
shadowTextColor = colors.gray, shadowTextColor = 'gray',
backgroundColor = colors.black, -- colors.lightGray, markBackgroundColor = 'gray',
backgroundFocusColor = colors.black, --lightGray, backgroundColor = 'black',
backgroundFocusColor = 'black',
height = 1, height = 1,
limit = 6, limit = 6,
cursorBlink = true,
accelerators = { accelerators = {
[ 'control-c' ] = 'copy', [ 'control-c' ] = 'copy',
} }
} }
function UI.TextEntry:postInit() function UI.TextEntry:postInit()
self.entry = entry({ limit = self.limit, offset = 2 }) self.entry = entry({ limit = self.limit, offset = 2, transform = transform(self.transform) })
end end
function UI.TextEntry:layout() function UI.TextEntry:layout()
@@ -34,15 +42,15 @@ function UI.TextEntry:layout()
end end
function UI.TextEntry:setValue(value) function UI.TextEntry:setValue(value)
self.value = value --or '' self.value = value
self.entry:unmark() self.entry:unmark()
self.entry.value = tostring(value) self.entry.value = value
self.entry:updateScroll() self.entry:updateScroll()
end end
function UI.TextEntry:setPosition(pos) function UI.TextEntry:setPosition(pos)
self.entry.pos = pos self.entry.pos = pos
self.entry.value = tostring(self.value or '') self.entry.value = self.value -- WHY HERE ?
self.entry:updateScroll() self.entry:updateScroll()
end end
@@ -66,7 +74,9 @@ function UI.TextEntry:draw()
text = self.shadowText text = self.shadowText
end end
self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc) local ss = self.entry.scroll > 0 and '\183' or ' '
self:write(2, 1, Util.widthify(text, self.width - 2) .. ' ', bg, tc)
self:write(1, 1, ss, bg, self.shadowTextColor)
if self.entry.mark.active then if self.entry.mark.active then
local tx = math.max(self.entry.mark.x - self.entry.scroll, 0) local tx = math.max(self.entry.mark.x - self.entry.scroll, 0)
@@ -77,7 +87,7 @@ function UI.TextEntry:draw()
end end
if tx ~= tex then if tx ~= tex then
self:write(tx + 2, 1, text:sub(tx + 1, tex), colors.gray, tc) self:write(tx + 2, 1, text:sub(tx + 1, tex), self.markBackgroundColor, tc)
end end
end end
if self.focused then if self.focused then
@@ -85,6 +95,8 @@ function UI.TextEntry:draw()
end end
end end
UI.TextEntry.docs.reset = [[reset()
Clears the value and resets the cursor.]]
function UI.TextEntry:reset() function UI.TextEntry:reset()
self.entry:reset() self.entry:reset()
self.value = nil--'' self.value = nil--''
@@ -96,34 +108,25 @@ function UI.TextEntry:updateCursor()
self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1) self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1)
end end
function UI.TextEntry:focus() function UI.TextEntry:markAll()
self:draw() self.entry:markAll()
if self.focused then
self:setCursorBlink(true)
else
self:setCursorBlink(false)
end
end end
function UI.TextEntry:_transform(text) function UI.TextEntry:focus()
if self.transform == 'lowercase' then self:draw()
return _lower(text)
elseif self.transform == 'uppercase' then
return _upper(text)
elseif self.transform == 'number' then
return tonumber(text) --or 0
end
return text
end end
function UI.TextEntry:eventHandler(event) function UI.TextEntry:eventHandler(event)
local text = self.value --or '' local text = self.value
self.entry.value = tostring(text or '') self.entry.value = text
if event.ie and self.entry:process(event.ie) then if event.ie and self.entry:process(event.ie) then
if self.entry.textChanged then if self.entry.textChanged then
self.value = self:_transform(self.entry.value) local changed = self.value ~= self.entry.value
self.value = self.entry.value
self:draw() self:draw()
if text ~= self.value then if changed then
-- we get entry.textChanged when marking is updated
-- no need to emit in that case
self:emit({ type = 'text_change', text = self.value, element = self }) self:emit({ type = 'text_change', text = self.value, element = self })
end end
elseif self.entry.posChanged then elseif self.entry.posChanged then
@@ -134,3 +137,35 @@ function UI.TextEntry:eventHandler(event)
return false return false
end end
function UI.TextEntry.example()
return UI.Window {
text = UI.TextEntry {
x = 2, y = 2,
width = 12,
limit = 36,
shadowText = 'normal',
},
upper = UI.TextEntry {
x = 2, y = 3,
width = 12,
limit = 36,
shadowText = 'upper',
transform = 'uppercase',
},
lower = UI.TextEntry {
x = 2, y = 4,
width = 12,
limit = 36,
shadowText = 'lower',
transform = 'lowercase',
},
number = UI.TextEntry {
x = 2, y = 5,
width = 12,
limit = 36,
transform = 'number',
shadowText = 'number',
},
}
end

View File

@@ -20,24 +20,15 @@ UI.Throttle.defaults = {
' //) (O ). @ \\-d ) (@ ' ' //) (O ). @ \\-d ) (@ '
} }
} }
function UI.Throttle:setParent() function UI.Throttle:layout()
self.x = math.ceil((self.parent.width - self.width) / 2) self.x = math.ceil((self.parent.width - self.width) / 2)
self.y = math.ceil((self.parent.height - self.height) / 2) self.y = math.ceil((self.parent.height - self.height) / 2)
UI.Window.setParent(self) self:reposition(self.x, self.y, self.width, self.height)
end end
function UI.Throttle:enable() function UI.Throttle:enable()
self.c = os.clock() self.c = os.clock()
self.enabled = false self.ctr = 0
end
function UI.Throttle:disable()
if self.canvas then
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
self.ctr = 0
end
end end
function UI.Throttle:update() function UI.Throttle:update()
@@ -46,11 +37,7 @@ function UI.Throttle:update()
os.sleep(0) os.sleep(0)
self.c = os.clock() self.c = os.clock()
self.enabled = true self.enabled = true
if not self.canvas then self:clear(self.borderColor)
self.canvas = self:addLayer(self.backgroundColor, self.borderColor)
self.canvas:setVisible(true)
self:clear(self.borderColor)
end
local image = self.image[self.ctr + 1] local image = self.image[self.ctr + 1]
local width = self.width - 2 local width = self.width - 2
for i = 0, #self.image do for i = 0, #self.image do
@@ -63,3 +50,25 @@ function UI.Throttle:update()
self:sync() self:sync()
end end
end end
function UI.Throttle.example()
return UI.Window {
button1 = UI.Button {
x = 2, y = 2,
text = 'Test',
},
throttle = UI.Throttle {
textColor = colors.yellow,
borderColor = colors.green,
},
eventHandler = function (self, event)
if event.type == 'button_press' then
for _ = 1, 40 do
self.throttle:update()
os.sleep(.05)
end
self.throttle:disable()
end
end,
}
end

View File

@@ -1,59 +1,20 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local _rep = string.rep
local _sub = string.sub
-- For manipulating text in a fixed width string
local SB = class()
function SB:init(width)
self.width = width
self.buf = _rep(' ', width)
end
function SB:insert(x, str, width)
if x < 1 then
x = self.width + x + 1
end
width = width or #str
if x + width - 1 > self.width then
width = self.width - x
end
if width > 0 then
self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width)
end
end
function SB:fill(x, ch, width)
width = width or self.width - x + 1
self:insert(x, _rep(ch, width))
end
function SB:center(str)
self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str)
end
function SB:get()
return self.buf
end
UI.TitleBar = class(UI.Window) UI.TitleBar = class(UI.Window)
UI.TitleBar.defaults = { UI.TitleBar.defaults = {
UIElement = 'TitleBar', UIElement = 'TitleBar',
height = 1, height = 1,
textColor = colors.white,
backgroundColor = colors.cyan,
title = '', title = '',
frameChar = UI.extChars and '\140' or '-', frameChar = UI.extChars and '\140' or '-',
closeInd = UI.extChars and '\215' or '*', closeInd = UI.extChars and '\215' or '*',
} }
function UI.TitleBar:draw() function UI.TitleBar:draw()
local sb = SB(self.width) self:fillArea(2, 1, self.width - 2, 1, self.frameChar)
sb:fill(2, self.frameChar, sb.width - 3) self:centeredWrite(1, string.format(' %s ', self.title))
sb:center(string.format(' %s ', self.title))
if self.previousPage or self.event then if self.previousPage or self.event then
sb:insert(-1, self.closeInd) self:write(self.width - 1, 1, ' ' .. self.closeInd)
else
sb:insert(-2, self.frameChar)
end end
self:write(1, 1, sb:get())
end end
function UI.TitleBar:eventHandler(event) function UI.TitleBar:eventHandler(event)
@@ -69,5 +30,74 @@ function UI.TitleBar:eventHandler(event)
end end
return true return true
end end
elseif event.type == 'mouse_down' then
self.anchor = { x = event.x, y = event.y, ox = self.parent.x, oy = self.parent.y, h = self.parent.height }
elseif event.type == 'mouse_drag' then
if self.expand == 'height' then
local d = event.dy
if self.anchor.h - d > 0 and self.anchor.oy + d > 0 then
self.parent:reposition(self.parent.x, self.anchor.oy + event.dy, self.width, self.anchor.h - d)
end
elseif self.moveable then
local d = event.dy
if self.anchor.oy + d > 0 and self.anchor.oy + d <= self.parent.parent.height then
self.parent:move(self.anchor.ox + event.dx, self.anchor.oy + event.dy)
end
end
end end
end end
function UI.TitleBar.example()
return UI.Window {
win1 = UI.Window {
x = 9, y = 2, ex = -7, ey = -3,
backgroundColor = 'green',
titleBar = UI.TitleBar {
title = 'A really, really, really long title', moveable = true,
},
button1 = UI.Button {
x = 2, y = 3,
text = 'Press',
},
focus = function (self)
self:raise()
end,
},
win2 = UI.Window {
x = 7, y = 3, ex = -9, ey = -2,
backgroundColor = 'orange',
titleBar = UI.TitleBar {
title = 'test', moveable = true,
event = 'none',
},
button1 = UI.Button {
x = 2, y = 3,
text = 'Press',
},
focus = function (self)
self:raise()
end,
},
draw = function(self, isBG)
for i = 1, self.height do
self:write(1, i, self.filler or '')
end
if not isBG then
for _,v in pairs(self.children) do
v:draw()
end
end
end,
enable = function (self)
require('opus.event').onInterval(.5, function()
self.filler = string.rep(string.char(math.random(33, 126)), self.width)
self:draw(true)
self:sync()
end)
UI.Window.enable(self)
end
}
end

View File

@@ -1,13 +1,11 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
UI.VerticalMeter = class(UI.Window) UI.VerticalMeter = class(UI.Window)
UI.VerticalMeter.defaults = { UI.VerticalMeter.defaults = {
UIElement = 'VerticalMeter', UIElement = 'VerticalMeter',
backgroundColor = colors.gray, backgroundColor = 'gray',
meterColor = colors.lime, meterColor = 'lime',
width = 1, width = 1,
value = 0, value = 0,
} }
@@ -16,3 +14,18 @@ function UI.VerticalMeter:draw()
self:clear() self:clear()
self:clearArea(1, height + 1, self.width, self.height, self.meterColor) self:clearArea(1, height + 1, self.width, self.height, self.meterColor)
end end
function UI.VerticalMeter.example()
return UI.VerticalMeter {
x = 2, width = 3, y = 2, ey = -2,
focus = function() end,
enable = function(self)
require('opus.event').onInterval(.25, function()
self.value = self.value == 100 and 0 or self.value + 5
self:draw()
self:sync()
end)
return UI.VerticalMeter.enable(self)
end
}
end

View File

@@ -1,17 +1,15 @@
local class = require('opus.class') local class = require('opus.class')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
--[[-- Viewport --]]--
UI.Viewport = class(UI.Window) UI.Viewport = class(UI.Window)
UI.Viewport.defaults = { UI.Viewport.defaults = {
UIElement = 'Viewport', UIElement = 'Viewport',
backgroundColor = colors.cyan,
accelerators = { accelerators = {
down = 'scroll_down', down = 'scroll_down',
up = 'scroll_up', up = 'scroll_up',
home = 'scroll_top', home = 'scroll_top',
left = 'scroll_left',
right = 'scroll_right',
[ 'end' ] = 'scroll_bottom', [ 'end' ] = 'scroll_bottom',
pageUp = 'scroll_pageUp', pageUp = 'scroll_pageUp',
[ 'control-b' ] = 'scroll_pageUp', [ 'control-b' ] = 'scroll_pageUp',
@@ -19,53 +17,60 @@ UI.Viewport.defaults = {
[ 'control-f' ] = 'scroll_pageDown', [ 'control-f' ] = 'scroll_pageDown',
}, },
} }
function UI.Viewport:layout() function UI.Viewport:postInit()
UI.Window.layout(self) if self.showScrollBar then
if not self.canvas then self.scrollBar = UI.ScrollBar()
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end end
end end
function UI.Viewport:enable() function UI.Viewport:setScrollPosition(offy, offx) -- argh - reverse
UI.Window.enable(self) local oldOffy = self.offy
self.canvas:setVisible(true) self.offy = math.max(offy, 0)
end self.offy = math.min(self.offy, math.max(#self.lines, self.height) - self.height)
if self.offy ~= oldOffy then
function UI.Viewport:disable()
UI.Window.disable(self)
self.canvas:setVisible(false)
end
function UI.Viewport:setScrollPosition(offset)
local oldOffset = self.offy
self.offy = math.max(offset, 0)
self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height)
if self.offy ~= oldOffset then
if self.scrollBar then if self.scrollBar then
self.scrollBar:draw() self.scrollBar:draw()
end end
self.canvas.offy = offset self.offy = offy
self.canvas:dirty() self:dirty(true)
end
local oldOffx = self.offx
self.offx = math.max(offx or 0, 0)
self.offx = math.min(self.offx, math.max(#self.lines[1], self.width) - self.width)
if self.offx ~= oldOffx then
if self.scrollBar then
--self.scrollBar:draw()
end
self.offx = offx or 0
self:dirty(true)
end end
end end
function UI.Viewport:write(x, y, text, bg, tc) function UI.Viewport:blit(x, y, text, bg, fg)
if y > #self.canvas.lines then if y > #self.lines then
for i = #self.canvas.lines, y do self:resizeBuffer(self.width, y)
self.canvas.lines[i + 1] = { } end
self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor) return UI.Window.blit(self, x, y, text, bg, fg)
end end
function UI.Viewport:write(x, y, text, bg, fg)
if y > #self.lines then
self:resizeBuffer(self.width, y)
end
return UI.Window.write(self, x, y, text, bg, fg)
end
function UI.Viewport:setViewHeight(h)
if h > #self.lines then
self:resizeBuffer(self.width, h)
end end
return UI.Window.write(self, x, y, text, bg, tc)
end end
function UI.Viewport:reset() function UI.Viewport:reset()
self.offy = 0 self.offy = 0
self.canvas.offy = 0 for i = self.height + 1, #self.lines do
for i = self.height + 1, #self.canvas.lines do self.lines[i] = nil
self.canvas.lines[i] = nil
end end
end end
@@ -73,26 +78,33 @@ function UI.Viewport:getViewArea()
return { return {
y = (self.offy or 0) + 1, y = (self.offy or 0) + 1,
height = self.height, height = self.height,
totalHeight = #self.canvas.lines, totalHeight = #self.lines,
offsetY = self.offy or 0, offsetY = self.offy or 0,
} }
end end
function UI.Viewport:eventHandler(event) function UI.Viewport:eventHandler(event)
if #self.lines <= self.height then
return
end
if event.type == 'scroll_down' then if event.type == 'scroll_down' then
self:setScrollPosition(self.offy + 1) self:setScrollPosition(self.offy + 1, self.offx)
elseif event.type == 'scroll_up' then elseif event.type == 'scroll_up' then
self:setScrollPosition(self.offy - 1) self:setScrollPosition(self.offy - 1, self.offx)
elseif event.type == 'scroll_left' then
self:setScrollPosition(self.offy, self.offx - 1)
elseif event.type == 'scroll_right' then
self:setScrollPosition(self.offy, self.offx + 1)
elseif event.type == 'scroll_top' then elseif event.type == 'scroll_top' then
self:setScrollPosition(0) self:setScrollPosition(0, 0)
elseif event.type == 'scroll_bottom' then elseif event.type == 'scroll_bottom' then
self:setScrollPosition(10000000) self:setScrollPosition(10000000, 0)
elseif event.type == 'scroll_pageUp' then elseif event.type == 'scroll_pageUp' then
self:setScrollPosition(self.offy - self.height) self:setScrollPosition(self.offy - self.height, self.offx)
elseif event.type == 'scroll_pageDown' then elseif event.type == 'scroll_pageDown' then
self:setScrollPosition(self.offy + self.height) self:setScrollPosition(self.offy + self.height, self.offx)
elseif event.type == 'scroll_to' then elseif event.type == 'scroll_to' then
self:setScrollPosition(event.offset) self:setScrollPosition(event.offset, 0)
else else
return false return false
end end

View File

@@ -25,9 +25,6 @@ function UI.Wizard:postInit()
} }
Util.merge(self, self.pages) Util.merge(self, self.pages)
--for _, child in pairs(self.pages) do
-- child.ey = -2
--end
end end
function UI.Wizard:add(pages) function UI.Wizard:add(pages)
@@ -50,9 +47,8 @@ end
function UI.Wizard:enable(...) function UI.Wizard:enable(...)
self.enabled = true self.enabled = true
self.index = 1 self.index = 1
self.transitionHint = nil
local initial = self:getPage(1) local initial = self:getPage(1)
for _,child in pairs(self.children) do for child in self:eachChild() do
if child == initial or not child.index then if child == initial or not child.index then
child:enable(...) child:enable(...)
else else
@@ -93,12 +89,13 @@ function UI.Wizard:eventHandler(event)
elseif event.type == 'enable_view' then elseif event.type == 'enable_view' then
local current = event.next or event.prev local current = event.next or event.prev
if not current then error('property "index" is required on wizard pages') end if not current then error('property "index" is required on wizard pages') end
local hint
if event.current then if event.current then
if event.next then if event.next then
self.transitionHint = 'slideLeft' hint = 'slideLeft'
elseif event.prev then elseif event.prev then
self.transitionHint = 'slideRight' hint = 'slideRight'
end end
event.current:disable() event.current:disable()
end end
@@ -117,8 +114,40 @@ function UI.Wizard:eventHandler(event)
self.nextButton.event = 'wizard_complete' self.nextButton.event = 'wizard_complete'
end end
-- a new current view -- a new current view
current.transitionHint = hint
current:enable() current:enable()
current:emit({ type = 'view_enabled', view = current }) current:emit({ type = 'view_enabled', view = current })
self:draw() self:draw()
end end
end end
function UI.Wizard.example()
return UI.Wizard {
ey = -2,
pages = {
splash = UI.WizardPage {
index = 1,
intro = UI.TextArea {
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = 'sample text',
},
},
label = UI.WizardPage {
index = 2,
intro = UI.TextArea {
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = 'sample more text',
},
},
password = UI.WizardPage {
index = 3,
text = UI.TextEntry {
x = 12, ex = -3, y = 2,
shadowText = 'tet',
},
},
},
}
end

Some files were not shown because too many files have changed in this diff Show More